@nihplod/sso-sdk 1.4.1 → 1.6.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/index.ts","../../src/react/SsoProvider.tsx","../../src/core/errors.ts","../../src/core/pkce.ts","../../src/core/storage.ts","../../src/core/security.ts","../../src/core/id-token.ts","../../src/core/SsoClient.ts","../../src/react/RequireAuth.tsx","../../src/react/CallbackPage.tsx"],"sourcesContent":["/**\r\n * @nihplod/sso-sdk/react\r\n *\r\n * React 绑定层:SsoProvider Context、useSso Hook、RequireAuth 路由保护、CallbackPage 回调处理。\r\n */\r\n\r\nexport { SsoProvider, useSso } from \"./SsoProvider\";\r\nexport { RequireAuth, withAuth } from \"./RequireAuth\";\r\nexport { CallbackPage, DefaultCallbackError } from \"./CallbackPage\";\r\nexport type { CallbackPageProps } from \"./CallbackPage\";\r\n","/**\r\n * SsoProvider - React Context Provider\r\n *\r\n * 包裹子项目根组件,提供全局 SSO 认证状态管理:\r\n * - 自动管理 token 刷新定时器(过期前 60s 静默刷新)\r\n * - 监听 storage 事件实现跨 Tab 同步\r\n * - 提供 useSso() hook\r\n */\r\n\r\n\"use client\";\r\n\r\nimport React, {\r\n createContext,\r\n useContext,\r\n useState,\r\n useEffect,\r\n useCallback,\r\n useRef,\r\n type ReactNode,\r\n} from \"react\";\r\nimport { SsoClient } from \"../core/SsoClient\";\r\nimport { getTokenData, removeTokenData } from \"../core/storage\";\r\nimport type { TokenData } from \"../core/storage\";\r\nimport { SsoError } from \"../core/errors\";\r\nimport type { SsoClientConfig, SsoUser } from \"../core/SsoClient\";\r\n\r\n// ============================================\r\n// Context 类型\r\n// ============================================\r\n\r\n/**\r\n * 认证状态为三态:\r\n * - isLoading=true:初始化/刷新中,user 与 error 均可能为 null\r\n * - error 非 null:加载用户信息失败(如会话已失效),user 为 null\r\n * - isAuthenticated=true 且 user 非 null:已登录\r\n */\r\nexport interface SsoContextValue {\r\n /** 当前用户信息 */\r\n user: SsoUser | null;\r\n\r\n /** 是否已认证 */\r\n isAuthenticated: boolean;\r\n\r\n /** 是否正在加载(初始化/刷新中) */\r\n isLoading: boolean;\r\n\r\n /** 最近一次加载用户信息失败的错误(成功或登出后为 null) */\r\n error: SsoError | null;\r\n\r\n /** 发起登录(同页重定向) */\r\n login: (returnUrl?: string) => Promise<void>;\r\n\r\n /** 弹窗模式登录(保持当前页面状态不丢失) */\r\n loginPopup: (options?: { returnUrl?: string; width?: number; height?: number }) => Promise<TokenData>;\r\n\r\n /** 登出 */\r\n logout: (redirectToSso?: boolean) => Promise<void>;\r\n\r\n /** 刷新用户信息 */\r\n refreshUser: () => Promise<void>;\r\n\r\n /** 获取 access_token(自动刷新过期 token) */\r\n getAccessToken: () => Promise<string | null>;\r\n\r\n /** SsoClient 实例(高级用法) */\r\n client: SsoClient;\r\n}\r\n\r\nconst SsoContext = createContext<SsoContextValue | null>(null);\r\n\r\n// ============================================\r\n// Provider Props\r\n// ============================================\r\n\r\nexport interface SsoProviderProps {\r\n /** SSO 客户端配置 */\r\n config: SsoClientConfig;\r\n\r\n /** 子组件 */\r\n children: ReactNode;\r\n\r\n /**\r\n * 自动刷新阈值(秒)\r\n * access_token 过期前多少秒触发静默刷新。\r\n * 默认 60 秒。\r\n */\r\n refreshThreshold?: number;\r\n\r\n /**\r\n * Token 静默刷新成功后的回调(可选)。\r\n * 每次刷新成功时以新的 access_token 调用,可用于同步 token 到外部状态;\r\n * SDK 不会据此自动重试先前失败的 API 请求,重试需由调用方自行实现。\r\n */\r\n onTokenRefreshed?: (token: string) => void;\r\n}\r\n\r\n// 跨 Tab 刷新锁(基于 localStorage + 时间戳,避免多 Tab 同时刷新导致旧 RT 被撤销)\r\nconst REFRESH_LOCK_PREFIX = \"nihplod_sso_refresh_lock:\";\r\nconst LOCK_TTL_MS = 5000;\r\n\r\n// 本 Tab 持有的锁 token(用于释放时校验所有权,避免误删其他 Tab 的锁)\r\nconst ownedLocks = new Map<string, string>();\r\n\r\nfunction lockKey(clientId: string): string {\r\n return REFRESH_LOCK_PREFIX + clientId;\r\n}\r\n\r\n/**\r\n * 尝试获取跨 Tab 刷新锁。\r\n *\r\n * 权衡说明:localStorage 没有原子的 check-and-set,多 Tab 并发时无法完全避免\r\n * 竞态(两个 Tab 同毫秒通过存在性检查后都写入)。这里采用「写入唯一 token 后\r\n * 立刻 read-back 校验」将竞态窗口缩到最小;残余的双刷新风险由 SSO 服务端\r\n * refresh_token 轮换宽限期兜底,属于可接受权衡。\r\n */\r\nfunction acquireRefreshLock(clientId: string): boolean {\r\n if (typeof localStorage === \"undefined\") return true;\r\n const key = lockKey(clientId);\r\n const now = Date.now();\r\n const raw = localStorage.getItem(key);\r\n if (raw) {\r\n const ts = parseInt(raw, 10);\r\n if (!isNaN(ts) && now - ts < LOCK_TTL_MS) {\r\n return false;\r\n }\r\n }\r\n const token = `${now}:${Math.random().toString(36).slice(2)}`;\r\n localStorage.setItem(key, token);\r\n // read-back:若读回的不是自己写入的值,说明其他 Tab 并发抢锁成功\r\n if (localStorage.getItem(key) !== token) return false;\r\n ownedLocks.set(clientId, token);\r\n return true;\r\n}\r\n\r\nfunction releaseRefreshLock(clientId: string): void {\r\n if (typeof localStorage === \"undefined\") return;\r\n const owned = ownedLocks.get(clientId);\r\n ownedLocks.delete(clientId);\r\n // 仅当锁仍归本 Tab 所有时才删除,避免误删其他 Tab 新获取的锁\r\n if (owned && localStorage.getItem(lockKey(clientId)) === owned) {\r\n localStorage.removeItem(lockKey(clientId));\r\n }\r\n}\r\n\r\n/**\r\n * 在跨 Tab 互斥锁下执行静默刷新。\r\n *\r\n * 优先使用 Web Locks API(navigator.locks)实现浏览器级真互斥:\r\n * 拿到锁的 Tab 执行刷新;其他 Tab 以 ifAvailable 模式拿到 null lock\r\n * 直接返回,等待 storage 事件同步刷新结果。navigator.locks 不存在\r\n * 或调用异常(如隐私模式禁用)时回退 localStorage 锁逻辑。\r\n *\r\n * @returns true 表示本 Tab 持锁执行了刷新;false 表示未抢到锁(其他 Tab 正在刷新)\r\n *\r\n * 注意:task 必须自行消化内部异常(不得向外抛出),以便\r\n * navigator.locks.request 的 reject 可安全视为锁 API 故障并回退。\r\n */\r\nexport async function withRefreshLock(clientId: string, task: () => Promise<void>): Promise<boolean> {\r\n if (\r\n typeof navigator !== \"undefined\" &&\r\n typeof navigator.locks?.request === \"function\"\r\n ) {\r\n try {\r\n let ran = false;\r\n await navigator.locks.request(\r\n `nihplod_sso_refresh_${clientId}`,\r\n { ifAvailable: true },\r\n async (lock) => {\r\n if (!lock) return;\r\n ran = true;\r\n await task();\r\n }\r\n );\r\n return ran;\r\n } catch {\r\n // Web Locks API 故障,回退 localStorage 锁\r\n }\r\n }\r\n\r\n if (!acquireRefreshLock(clientId)) return false;\r\n try {\r\n await task();\r\n return true;\r\n } finally {\r\n releaseRefreshLock(clientId);\r\n }\r\n}\r\n\r\n// ============================================\r\n// Provider 实现\r\n// ============================================\r\n\r\nexport function SsoProvider({\r\n config,\r\n children,\r\n refreshThreshold = 60,\r\n onTokenRefreshed,\r\n}: SsoProviderProps) {\r\n const [user, setUser] = useState<SsoUser | null>(null);\r\n const [isLoading, setIsLoading] = useState(true);\r\n const [isAuthenticated, setIsAuthenticated] = useState(false);\r\n const [error, setError] = useState<SsoError | null>(null);\r\n // 实例用 useState 懒初始化保持稳定引用(避免渲染期读取 ref)\r\n const [client] = useState(() => new SsoClient(config));\r\n const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n const loadedRef = useRef(false);\r\n\r\n // 加载用户信息\r\n const loadUser = useCallback(async () => {\r\n // 以「本地存在 token 数据」为准而非 isAuthenticated():\r\n // access_token 已过期时 getTokenData 仍返回数据(保留 refresh_token),\r\n // getUserInfo 内部会自动刷新恢复会话,避免过期即丢登录态。\r\n const tokenData = getTokenData(client.config.clientId);\r\n if (!tokenData) {\r\n setUser(null);\r\n setIsAuthenticated(false);\r\n setError(null);\r\n setIsLoading(false);\r\n return;\r\n }\r\n\r\n try {\r\n const u = await client.getUserInfo();\r\n setUser(u);\r\n setIsAuthenticated(true);\r\n setError(null);\r\n } catch (err) {\r\n setUser(null);\r\n setIsAuthenticated(false);\r\n // 写入 error 状态,调用方可据此展示\"会话失效,请重新登录\"等提示\r\n setError(\r\n err instanceof SsoError\r\n ? err\r\n : new SsoError(\"userinfo_failed\", err instanceof Error ? err.message : String(err))\r\n );\r\n // 仅鉴权类错误(会话失效/未认证)清除本地 token;网络瞬断等可恢复错误\r\n // 保留 token,避免一次抖动就强制重新登录,下次加载/刷新会自动重试\r\n if (\r\n err instanceof SsoError &&\r\n (err.code === \"not_authenticated\" || err.code === \"session_expired\")\r\n ) {\r\n removeTokenData(client.config.clientId);\r\n }\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n }, [client]);\r\n\r\n // 初始化(微任务延迟,避免 effect 内同步 setState)\r\n useEffect(() => {\r\n if (loadedRef.current) return;\r\n loadedRef.current = true;\r\n\r\n Promise.resolve().then(() => {\r\n // 本地存在 token 数据即尝试加载(过期 token 会在 loadUser 内自动刷新)\r\n if (getTokenData(client.config.clientId)) {\r\n loadUser();\r\n } else {\r\n setIsLoading(false);\r\n }\r\n });\r\n }, [client, loadUser]);\r\n\r\n // Token 自动刷新定时器(setTimeout 递归调度,避免 setInterval 堆积)\r\n useEffect(() => {\r\n let active = true;\r\n\r\n // 跨 Tab 互斥下的静默刷新:拿到锁的 Tab 执行刷新,\r\n // 刷新失败可能是其他 Tab 已刷新,短暂后重新加载本地 token\r\n const attemptRefresh = () =>\r\n withRefreshLock(client.config.clientId, async () => {\r\n try {\r\n const td = await client.refreshToken();\r\n onTokenRefreshed?.(td.access_token);\r\n loadUser();\r\n } catch {\r\n setTimeout(() => loadUser(), 500);\r\n }\r\n });\r\n\r\n const scheduleNextRefresh = () => {\r\n if (!active) return;\r\n\r\n // 清除已有定时器\r\n if (refreshTimerRef.current) {\r\n clearTimeout(refreshTimerRef.current);\r\n refreshTimerRef.current = null;\r\n }\r\n\r\n const tokenData = getTokenData(client.config.clientId);\r\n if (!tokenData) return;\r\n\r\n // 计算距离过期还有多少秒\r\n const remainingSec = (tokenData.expires_at - Date.now()) / 1000;\r\n\r\n if (remainingSec <= 0) {\r\n // 已过期,立即刷新(带锁,避免多 Tab 并发)\r\n void attemptRefresh();\r\n return;\r\n }\r\n\r\n if (remainingSec <= refreshThreshold) {\r\n // 即将过期,立即刷新(带锁)\r\n void attemptRefresh();\r\n return;\r\n }\r\n\r\n // 在过期前 refreshThreshold 秒触发刷新\r\n const delayMs = (remainingSec - refreshThreshold) * 1000;\r\n\r\n refreshTimerRef.current = setTimeout(() => {\r\n if (!active) return;\r\n const td = getTokenData(client.config.clientId);\r\n if (!td) return;\r\n\r\n const secLeft = (td.expires_at - Date.now()) / 1000;\r\n if (secLeft <= refreshThreshold) {\r\n void attemptRefresh().then((didRefresh) => {\r\n // 未抢到锁(其他 Tab 正在刷新)时不重新调度,等待 storage 事件同步\r\n if (didRefresh) scheduleNextRefresh();\r\n });\r\n }\r\n }, Math.max(delayMs, 1000));\r\n };\r\n\r\n scheduleNextRefresh();\r\n\r\n return () => {\r\n active = false;\r\n if (refreshTimerRef.current) {\r\n clearTimeout(refreshTimerRef.current);\r\n refreshTimerRef.current = null;\r\n }\r\n };\r\n }, [client, loadUser, refreshThreshold, onTokenRefreshed]);\r\n\r\n // 监听 storage 事件实现跨 Tab 同步\r\n useEffect(() => {\r\n const handleStorageChange = (e: StorageEvent) => {\r\n if (\r\n e.key &&\r\n e.key.startsWith(\"nihplod_sso_\") &&\r\n !e.key.startsWith(REFRESH_LOCK_PREFIX)\r\n ) {\r\n loadUser();\r\n }\r\n };\r\n\r\n window.addEventListener(\"storage\", handleStorageChange);\r\n return () => window.removeEventListener(\"storage\", handleStorageChange);\r\n }, [loadUser]);\r\n\r\n // 登录\r\n const login = useCallback(\r\n async (returnUrl?: string) => {\r\n await client.login(returnUrl);\r\n },\r\n [client]\r\n );\r\n\r\n // 弹窗模式登录\r\n const loginPopup = useCallback(\r\n async (options?: { returnUrl?: string; width?: number; height?: number }) => {\r\n const tokenData = await client.loginPopup(options);\r\n await loadUser();\r\n return tokenData;\r\n },\r\n [client, loadUser]\r\n );\r\n\r\n // 登出\r\n const logout = useCallback(\r\n async (redirectToSso: boolean = false) => {\r\n await client.logout(redirectToSso);\r\n setUser(null);\r\n setIsAuthenticated(false);\r\n setError(null);\r\n },\r\n [client]\r\n );\r\n\r\n // 刷新用户信息\r\n const refreshUser = useCallback(async () => {\r\n await loadUser();\r\n }, [loadUser]);\r\n\r\n // 获取 access_token\r\n const getAccessToken = useCallback(async (): Promise<string | null> => {\r\n return client.getAccessToken();\r\n }, [client]);\r\n\r\n const value: SsoContextValue = {\r\n user,\r\n isAuthenticated,\r\n isLoading,\r\n error,\r\n login,\r\n loginPopup,\r\n logout,\r\n refreshUser,\r\n getAccessToken,\r\n client,\r\n };\r\n\r\n return React.createElement(SsoContext.Provider, { value }, children);\r\n}\r\n\r\n// ============================================\r\n// Hook\r\n// ============================================\r\n\r\n/**\r\n * useSso Hook\r\n *\r\n * 在 SsoProvider 内部使用,获取 SSO 认证状态和操作方法。\r\n *\r\n * @example\r\n * ```tsx\r\n * function MyComponent() {\r\n * const { user, isAuthenticated, login, logout } = useSso();\r\n * if (!isAuthenticated) return <button onClick={() => login()}>登录</button>;\r\n * return <div>欢迎, {user?.nickname}</div>;\r\n * }\r\n * ```\r\n */\r\nexport function useSso(): SsoContextValue {\r\n const ctx = useContext(SsoContext);\r\n if (!ctx) {\r\n throw new Error(\"useSso() 必须在 <SsoProvider> 内部使用\");\r\n }\r\n return ctx;\r\n}\r\n","/**\r\n * SSO SDK 错误类型\r\n */\r\n\r\n/** SSO 错误码 */\r\nexport type SsoErrorCode =\r\n | \"invalid_config\"\r\n | \"state_mismatch\"\r\n | \"pkce_required\"\r\n | \"token_request_failed\"\r\n | \"session_expired\"\r\n | \"no_refresh_token\"\r\n | \"userinfo_failed\"\r\n | \"not_authenticated\"\r\n | \"authorization_code_expired\"\r\n // 保留:当前 SDK 不主动抛出,预留给服务端错误透传/未来版本使用\r\n | \"authorization_code_used\"\r\n | \"client_disabled\"\r\n | \"user_denied_authorization\"\r\n // 保留:当前 SDK 不主动抛出,预留给服务端错误透传/未来版本使用\r\n | \"account_disabled\"\r\n | \"sso_server_error\"\r\n | \"rate_limited\"\r\n | \"network_error\"\r\n | \"popup_blocked\"\r\n | \"popup_closed\"\r\n | \"id_token_invalid\"\r\n | \"id_token_unsupported_alg\"\r\n | \"id_token_hs256_unsupported\"\r\n // 保留:当前 SDK 不主动抛出(HS256 一律拒绝,见 id_token_hs256_unsupported)\r\n | \"id_token_missing_secret\"\r\n | \"id_token_invalid_signature\"\r\n | \"id_token_issuer_mismatch\"\r\n | \"id_token_audience_mismatch\"\r\n | \"id_token_expired\"\r\n | \"id_token_missing_sub\"\r\n | \"id_token_at_hash_mismatch\"\r\n | \"id_token_nonce_mismatch\"\r\n | \"logout_token_invalid\"\r\n | \"logout_token_unsupported_alg\"\r\n | \"logout_token_invalid_signature\"\r\n | \"logout_token_issuer_mismatch\"\r\n | \"logout_token_audience_mismatch\"\r\n | \"logout_token_expired\"\r\n | \"logout_token_replay\";\r\n\r\n/**\r\n * 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode\r\n *\r\n * @param oauthError 服务端返回的 error 字段\r\n * @param context 调用上下文:\"token_exchange\"(授权码换 token)或 \"refresh\"(刷新)。\r\n * invalid_grant 在换 token 场景多为授权码过期/已用,在刷新场景多为会话失效,\r\n * 需按上下文细化映射。\r\n */\r\nexport function mapOAuthErrorToSsoCode(\r\n oauthError: string,\r\n context: \"token_exchange\" | \"refresh\" = \"token_exchange\"\r\n): SsoErrorCode {\r\n switch (oauthError) {\r\n case \"invalid_grant\":\r\n // 刷新场景的 invalid_grant 表示 refresh_token 已撤销/过期(调用方另会清除本地登录态)\r\n return context === \"refresh\" ? \"session_expired\" : \"authorization_code_expired\";\r\n case \"invalid_client\":\r\n return \"client_disabled\";\r\n case \"access_denied\":\r\n return \"user_denied_authorization\";\r\n case \"server_error\":\r\n return \"sso_server_error\";\r\n case \"rate_limited\":\r\n // 限流是独立的可重试错误,不应误报为 network_error\r\n return \"rate_limited\";\r\n case \"invalid_scope\":\r\n case \"invalid_request\":\r\n case \"unauthorized_client\":\r\n case \"unsupported_grant_type\":\r\n default:\r\n return \"token_request_failed\";\r\n }\r\n}\r\n\r\n/**\r\n * SSO SDK 自定义错误\r\n */\r\nexport class SsoError extends Error {\r\n public readonly code: SsoErrorCode;\r\n public readonly description: string;\r\n public readonly cause?: unknown;\r\n\r\n constructor(code: SsoErrorCode, description: string, cause?: unknown) {\r\n super(`[SSO SDK] ${code}: ${description}`);\r\n this.name = \"SsoError\";\r\n this.code = code;\r\n this.description = description;\r\n this.cause = cause;\r\n }\r\n}\r\n\r\n/**\r\n * OAuth 2.0 协议层错误(RFC 6749 §5.2)\r\n *\r\n * 保留用于表示服务端原样返回的 OAuth 错误(error / error_description / error_uri),\r\n * 与 SDK 语义的 SsoError 区分。当前 SDK 内部不抛出此类型(统一使用 SsoError +\r\n * mapOAuthErrorToSsoCode 映射),作为公共类型导出供子项目自行解析 OAuth 响应时使用。\r\n */\r\nexport class OAuthError extends Error {\r\n public readonly code: string;\r\n public readonly description: string;\r\n public readonly uri?: string;\r\n\r\n constructor(code: string, description: string, uri?: string) {\r\n super(`[OAuth] ${code}: ${description}`);\r\n this.name = \"OAuthError\";\r\n this.code = code;\r\n this.description = description;\r\n this.uri = uri;\r\n }\r\n}\r\n","/**\r\n * PKCE (Proof Key for Code Exchange) 工具函数\r\n *\r\n * 实现 RFC 7636 规范:\r\n * - code_verifier: 43-128 字符的随机字符串\r\n * - code_challenge: SHA-256(code_verifier) 的 base64url 编码\r\n *\r\n * 仅支持 S256 模式,不提供 plain 模式。\r\n */\r\n\r\nconst VALID_CHARS =\r\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~\";\r\n\r\n/**\r\n * 生成密码学安全随机字符串\r\n *\r\n * 使用拒绝采样法(rejection sampling)避免取模非均匀分布。\r\n * VALID_CHARS 有 66 个字符,Uint8(0-255) 取模会偏向前 58 个字符。\r\n * 丢弃 >=198 的字节值使分布完全均匀(198 = floor(256/66) * 66)。\r\n */\r\nfunction generateRandomString(length: number): string {\r\n const mask = 198; // 66 * 3,最大均匀取值\r\n let result = \"\";\r\n\r\n while (result.length < length) {\r\n // 一次生成分块避免过小 array 的循环开销\r\n const chunkSize = Math.min(length * 2, 256);\r\n const array = new Uint8Array(chunkSize);\r\n crypto.getRandomValues(array);\r\n\r\n for (let i = 0; i < chunkSize && result.length < length; i++) {\r\n if (array[i] >= mask) continue; // 拒绝采样:丢弃 >=198 的字节\r\n result += VALID_CHARS[array[i] % VALID_CHARS.length];\r\n }\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * 生成 code_verifier\r\n *\r\n * 生成 43-128 字符之间的安全随机字符串,\r\n * 使用 `crypto.getRandomValues` 保证密码学安全性。\r\n *\r\n * @param length - 长度(默认 64),范围 43-128\r\n */\r\nexport function generateCodeVerifier(length: number = 64): string {\r\n if (length < 43 || length > 128) {\r\n throw new Error(\"code_verifier length must be between 43 and 128\");\r\n }\r\n return generateRandomString(length);\r\n}\r\n\r\n/**\r\n * 计算 code_challenge (S256)\r\n *\r\n * 对 code_verifier 进行 SHA-256 哈希,然后进行 base64url 编码。\r\n *\r\n * @param codeVerifier - PKCE code_verifier\r\n */\r\nexport async function generateCodeChallenge(\r\n codeVerifier: string\r\n): Promise<string> {\r\n const encoder = new TextEncoder();\r\n const data = encoder.encode(codeVerifier);\r\n const hash = await crypto.subtle.digest(\"SHA-256\", data);\r\n\r\n // 转为 base64url(无 padding)\r\n const bytes = new Uint8Array(hash);\r\n let binary = \"\";\r\n for (let i = 0; i < bytes.byteLength; i++) {\r\n binary += String.fromCharCode(bytes[i]);\r\n }\r\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\r\n}\r\n\r\n/**\r\n * 生成 state 参数\r\n *\r\n * 32 字节 hex 随机字符串,用于 CSRF 防护。\r\n */\r\nexport function generateState(): string {\r\n const array = new Uint8Array(32);\r\n crypto.getRandomValues(array);\r\n return Array.from(array, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\r\n}\r\n","/**\n * Token 存储抽象层\n *\n * 分两类存储:\n * - Token 数据:默认使用 sessionStorage(标签页级持久化)。登录回调后 CallbackPage\n * 默认整页跳转,内存存储会丢失登录态,因此默认改为 sessionStorage:\n * 整页跳转与刷新后登录态保留,关闭标签页自动清除;SSR / 隐私模式写入失败时\n * 降级为内存 Map。对需要多 Tab 共享 token 或 BFF/Confidential Client 场景,\n * 可通过 setTokenStorage() 注入 localStorage 实现(如 createSecureStorage({ persist: true }))。\n * - 临时数据(PKCE verifier / state / OIDC nonce / returnUrl / popup nonce):必须跨整页重定向存活\n * (login() 会 302 跳转到 SSO 中心再回来),因此默认写入 sessionStorage;\n * SSR 等无 sessionStorage 环境自动降级为内存 Map。\n *\n * 多 client 隔离:\n * - token / state / return_url 均支持按 clientId 隔离 key\n * - 不传 clientId 时使用全局 key,保持向后兼容\n */\n\n/** Token 数据 */\nexport interface TokenData {\n access_token: string;\n token_type: string;\n expires_in: number;\n refresh_token: string;\n id_token?: string;\n\n /** Token 签发时间(epoch ms),用于计算过期 */\n issued_at: number;\n\n /** 访问令牌实际过期时间(epoch ms)= issued_at + expires_in * 1000 */\n expires_at: number;\n}\n\n/** 存储接口 */\nexport interface TokenStorage {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\n/** 默认 localStorage/sessionStorage key 前缀 */\nconst STORAGE_PREFIX = \"nihplod_sso_\";\n\n/** 常量 key 名称 */\nconst TOKEN_KEY = \"token\";\nconst VERIFIER_KEY_PREFIX = \"pkce_verifier_\";\nconst STATE_KEY = \"oauth_state\";\nconst NONCE_KEY = \"oidc_nonce\";\nconst RETURN_URL_KEY = \"return_url\";\nconst LOGOUT_STATE_KEY = \"logout_state\";\n// 静默登录探测标记(prompt=none 发起的 authorize 请求所携带的 state)\nconst SILENT_PROBE_KEY = \"silent_probe\";\n\nfunction buildKey(base: string, clientId?: string): string {\n return clientId ? `${base}:${clientId}` : base;\n}\n\n/**\n * 内存存储适配器\n *\n * 仅作为 sessionStorage 不可用(SSR / 隐私模式写入失败)时的降级方案,\n * 或显式注入使用。页面刷新后用户需重新登录。\n *\n * 代价:多 Tab 间不会自动同步 token;SDK 内部已用锁机制避免并发刷新。\n */\nfunction createMemoryStorageAdapter(): TokenStorage {\n const store = new Map<string, string>();\n return {\n get(key: string) {\n return store.get(key) ?? null;\n },\n set(key: string, value: string) {\n store.set(key, value);\n },\n remove(key: string) {\n store.delete(key);\n },\n };\n}\n\nconst memoryStorageAdapter = createMemoryStorageAdapter();\n\n/**\n * 浏览器 localStorage 实现\n *\n * 适用于:\n * - Next.js BFF / Confidential Client(refresh_token 不直接暴露给浏览器)。\n * - 需要多 Tab 共享 token 的场景。\n *\n * 注意:Public Client 在浏览器中直接存储 refresh_token 会增加 XSS 风险,\n * 建议改用内存存储或 Service Worker 封装。\n */\nconst localStorageAdapter: TokenStorage = {\n get(key: string) {\n if (typeof localStorage === \"undefined\") return null;\n return localStorage.getItem(STORAGE_PREFIX + key);\n },\n set(key: string, value: string) {\n if (typeof localStorage === \"undefined\") return;\n try {\n localStorage.setItem(STORAGE_PREFIX + key, value);\n } catch (err) {\n // QuotaExceededError 等写入失败不应中断登录流程\n console.warn(\n `[SSO SDK] localStorage 写入失败(${err instanceof Error ? err.name : String(err)}),数据未持久化`\n );\n }\n },\n remove(key: string) {\n if (typeof localStorage === \"undefined\") return;\n localStorage.removeItem(STORAGE_PREFIX + key);\n },\n};\n\n/**\n * 浏览器 sessionStorage 实现\n *\n * 同时承担两个角色:\n * - Token 数据的默认存储:整页重定向(CallbackPage 默认行为)与页面刷新后\n * 登录态仍保留;sessionStorage 随标签页关闭自动清除,且不跨 Tab 共享,\n * 比 localStorage 缩小了 XSS 窃取 refresh_token 的暴露面。\n * - 临时数据(PKCE verifier / state / returnUrl / popup nonce)专用:\n * 必须跨整页重定向存活,不能放模块级内存。\n * SSR / 隐私模式写入失败时降级为内存 Map,保证不抛异常。\n */\nfunction createSessionStorageAdapter(): TokenStorage {\n const fallback = new Map<string, string>();\n return {\n get(key: string) {\n if (typeof sessionStorage !== \"undefined\") {\n return sessionStorage.getItem(STORAGE_PREFIX + key) ?? fallback.get(key) ?? null;\n }\n return fallback.get(key) ?? null;\n },\n set(key: string, value: string) {\n if (typeof sessionStorage === \"undefined\") {\n fallback.set(key, value);\n return;\n }\n try {\n sessionStorage.setItem(STORAGE_PREFIX + key, value);\n } catch {\n // 隐私模式等场景写入失败时降级到内存\n fallback.set(key, value);\n }\n },\n remove(key: string) {\n fallback.delete(key);\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.removeItem(STORAGE_PREFIX + key);\n },\n };\n}\n\n/** 临时数据存储(跨整页重定向存活),独立于 token 存储,不受 setTokenStorage() 影响 */\nconst _transient: TokenStorage = createSessionStorageAdapter();\n\n/**\n * 创建存储实现\n *\n * @param options.persist 是否持久化到 localStorage。默认 false(sessionStorage)。\n * - false(默认):sessionStorage,标签页级持久化(刷新/整页跳转后登录态保留,\n * 关闭标签页即清除),多 Tab 间不共享;隐私模式等写入失败时降级为内存。\n * - true:localStorage,多 Tab 共享、关闭浏览器后仍保留。\n * ⚠️ 安全警告:persist=true 会将 refresh_token 明文写入 localStorage,\n * 任何 XSS 均可在不被检测的情况下读取。仅在 BFF/Confidential Client\n * 且 refresh_token 不直接暴露给浏览器的场景下使用。\n */\nexport function createSecureStorage(options: { persist?: boolean } = {}): TokenStorage {\n return options.persist ? localStorageAdapter : createSessionStorageAdapter();\n}\n\n/**\n * 默认 token 存储:sessionStorage(标签页级持久化),SSR/隐私模式降级为内存。\n * 可通过 setTokenStorage() 覆盖。\n */\nlet _storage: TokenStorage = createSessionStorageAdapter();\n\n/**\n * 设置自定义存储实现\n */\nexport function setTokenStorage(storage: TokenStorage): void {\n _storage = storage;\n}\n\n/**\n * 获取当前存储实现\n */\nexport function getTokenStorage(): TokenStorage {\n return _storage;\n}\n\n// ============================================\n// Token 存取\n// ============================================\n\nexport function saveTokenData(data: TokenData, clientId?: string): void {\n _storage.set(buildKey(TOKEN_KEY, clientId), JSON.stringify(data));\n}\n\nexport function getTokenData(clientId?: string): TokenData | null {\n const raw = _storage.get(buildKey(TOKEN_KEY, clientId));\n if (!raw) return null;\n try {\n // 过期不物理删除:refresh_token 通常比 access_token 长寿,\n // 删除会同时丢弃 refresh_token 使刷新路径失效。\n // 是否过期由调用方依据 expires_at 判断(isAuthenticated / getUserInfo 等)。\n return JSON.parse(raw) as TokenData;\n } catch {\n return null;\n }\n}\n\nexport function removeTokenData(clientId?: string): void {\n _storage.remove(buildKey(TOKEN_KEY, clientId));\n}\n\n// ============================================\n// PKCE 临时数据存取(transient:sessionStorage,跨整页重定向存活)\n// ============================================\n\nexport function savePkceVerifier(clientId: string, verifier: string): void {\n _transient.set(VERIFIER_KEY_PREFIX + clientId, verifier);\n}\n\nexport function getPkceVerifier(clientId: string): string | null {\n return _transient.get(VERIFIER_KEY_PREFIX + clientId);\n}\n\nexport function removePkceVerifier(clientId: string): void {\n _transient.remove(VERIFIER_KEY_PREFIX + clientId);\n}\n\n// ============================================\n// State 参数存取(transient)\n// ============================================\n\nexport function saveOAuthState(state: string, clientId?: string): void {\n _transient.set(buildKey(STATE_KEY, clientId), state);\n}\n\nexport function getOAuthState(clientId?: string): string | null {\n return _transient.get(buildKey(STATE_KEY, clientId));\n}\n\nexport function removeOAuthState(clientId?: string): void {\n _transient.remove(buildKey(STATE_KEY, clientId));\n}\n\n// ============================================\n// OIDC nonce 存取(transient,ID Token 重放防护)\n// ============================================\n\nexport function saveOAuthNonce(nonce: string, clientId?: string): void {\n _transient.set(buildKey(NONCE_KEY, clientId), nonce);\n}\n\nexport function getOAuthNonce(clientId?: string): string | null {\n return _transient.get(buildKey(NONCE_KEY, clientId));\n}\n\nexport function removeOAuthNonce(clientId?: string): void {\n _transient.remove(buildKey(NONCE_KEY, clientId));\n}\n\n// ============================================\n// Logout State 存取(transient,RP-Initiated Logout CSRF 防护)\n// ============================================\n\nexport function saveLogoutState(state: string, clientId?: string): void {\n _transient.set(buildKey(LOGOUT_STATE_KEY, clientId), state);\n}\n\nexport function getLogoutState(clientId?: string): string | null {\n return _transient.get(buildKey(LOGOUT_STATE_KEY, clientId));\n}\n\nexport function removeLogoutState(clientId?: string): void {\n _transient.remove(buildKey(LOGOUT_STATE_KEY, clientId));\n}\n\n// ============================================\n// 返回 URL 存取(transient)\n// ============================================\nexport function saveReturnUrl(url: string, clientId?: string): void {\n _transient.set(buildKey(RETURN_URL_KEY, clientId), url);\n}\n\nexport function getReturnUrl(clientId?: string): string | null {\n return _transient.get(buildKey(RETURN_URL_KEY, clientId));\n}\n\nexport function removeReturnUrl(clientId?: string): void {\n _transient.remove(buildKey(RETURN_URL_KEY, clientId));\n}\n\n// ============================================\n// 静默登录探测标记存取(transient,prompt=none)\n// ============================================\n// 存的是该次探测 authorize 请求的 state 值,回调时用于匹配\n// (只有 state 与标记一致才按\"探测无会话\"处理,防伪造回调)\n\nexport function saveSilentProbe(state: string, clientId?: string): void {\n _transient.set(buildKey(SILENT_PROBE_KEY, clientId), state);\n}\n\nexport function getSilentProbe(clientId?: string): string | null {\n return _transient.get(buildKey(SILENT_PROBE_KEY, clientId));\n}\n\nexport function removeSilentProbe(clientId?: string): void {\n _transient.remove(buildKey(SILENT_PROBE_KEY, clientId));\n}\n\n// ============================================\n// 清理所有 SSO 数据\n// ============================================\n\nexport function clearAllSsoData(clientId?: string): void {\n if (clientId) {\n removeTokenData(clientId);\n removeOAuthState(clientId);\n removeOAuthNonce(clientId);\n removeReturnUrl(clientId);\n removeLogoutState(clientId);\n removeSilentProbe(clientId);\n removePkceVerifier(clientId);\n // popup nonce 复用 verifier key 空间(`${clientId}_popup_nonce`)\n removePkceVerifier(`${clientId}_popup_nonce`);\n return;\n }\n\n removeTokenData();\n removeOAuthState();\n removeOAuthNonce();\n removeReturnUrl();\n removeLogoutState();\n removeSilentProbe();\n\n // 清理所有 PKCE verifier:当前版本存于 sessionStorage,同时清 localStorage 中可能的旧版本残留\n const prefix = STORAGE_PREFIX + VERIFIER_KEY_PREFIX;\n const stores = [\n typeof sessionStorage !== \"undefined\" ? sessionStorage : null,\n typeof localStorage !== \"undefined\" ? localStorage : null,\n ];\n for (const store of stores) {\n if (!store) continue;\n const keys: string[] = [];\n for (let i = 0; i < store.length; i++) {\n const key = store.key(i);\n if (key?.startsWith(prefix)) keys.push(key);\n }\n for (const key of keys) {\n _transient.remove(key.slice(STORAGE_PREFIX.length));\n store.removeItem(key);\n }\n }\n}\n\n/**\n * 清理指定 clientId 列表的 PKCE verifier\n *\n * 适用于非浏览器环境或已知 clientId 场景。\n */\nexport function clearVerifiersForClients(clientIds: string[]): void {\n for (const clientId of clientIds) {\n _transient.remove(VERIFIER_KEY_PREFIX + clientId);\n }\n}\n","/**\n * 安全相关小工具:returnUrl 开放重定向校验、常量时间字符串比较\n */\n\n/**\n * 校验 returnUrl 是否可信(防开放重定向)。\n * 仅允许:相对路径(且不以 // 开头)或与 currentOrigin 完全同源的绝对 URL。\n * 拒绝一切含反斜杠 \\ 的值:浏览器会把 \"/\\evil.com\" 归一化为 \"//evil.com\",\n * 形成协议相对 URL 开放重定向。\n */\nexport function isTrustedReturnUrl(url: string, currentOrigin: string): boolean {\n if (!url) return false;\n if (url.includes(\"\\\\\")) return false;\n if (url.startsWith(\"/\") && !url.startsWith(\"//\")) return true;\n try {\n return new URL(url).origin === currentOrigin;\n } catch {\n return false;\n }\n}\n\n/**\n * 常量时间字符串比较(避免 state / at_hash / nonce 等机密值的时序侧信道)。\n * 长度不同也执行完整循环,不提前返回。\n */\nexport function timingSafeEqualString(a: string, b: string): boolean {\n const ba = new TextEncoder().encode(a);\n const bb = new TextEncoder().encode(b);\n if (ba.length === 0 || bb.length === 0) return ba.length === bb.length;\n const len = Math.max(ba.length, bb.length);\n let diff = ba.length ^ bb.length;\n for (let i = 0; i < len; i++) {\n diff |= ba[i % ba.length] ^ bb[i % bb.length];\n }\n return diff === 0;\n}\n","/**\n * ID Token 验证工具(浏览器 / Edge Runtime 通用)\n *\n * 提取 SsoClient 与 Next.js callback 中重复的 ID Token 校验逻辑,\n * 包括 base64url 解码、JWKS 获取、RS256 签名验证、at_hash 计算等。\n */\n\nimport { SsoError } from \"./errors\";\nimport { timingSafeEqualString } from \"./security\";\n\n// 运行时 crypto.subtle 检测:现代浏览器 / Edge Runtime / Node 20+ 均提供全局 crypto\nconst _crypto: Pick<Crypto, \"subtle\"> | null =\n typeof crypto !== \"undefined\" && (crypto as Crypto).subtle ? (crypto as Crypto) : null;\n\nfunction getCrypto(): Crypto | null {\n return (_crypto as Crypto | null) ?? null;\n}\n\nexport interface JwksKey {\n kty: string;\n kid?: string;\n alg?: string;\n use?: string;\n n?: string;\n e?: string;\n x5c?: string[];\n}\n\nexport interface Jwks {\n keys: JwksKey[];\n}\n\nexport interface ValidateIdTokenResult {\n sub: string;\n}\n\nexport interface ValidateIdTokenOptions {\n /**\n * 发现 SSO 中心已启用 RS256 时,是否拒绝 HS256 ID Token。\n * 默认 true:只要 JWKS 中存在 RS256 签名密钥,就拒绝 HS256(安全推荐)。\n */\n rejectHs256WhenRs256Available?: boolean;\n /**\n * 期望的 OIDC nonce(login 时生成并随 authorize 请求发送)。\n * 传入后 ID Token 必须携带 nonce claim 且与该值常量时间相等(fail-closed),\n * 否则抛出 id_token_nonce_mismatch,防止 ID Token 重放。\n * 不传则跳过 nonce 校验(如 refresh 流程签发的 ID Token)。\n */\n expectedNonce?: string;\n}\n\nfunction base64UrlDecode(input: string): Uint8Array {\n const base64 = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\n/** 解码 JWT payload(不验证签名) */\nexport function decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n const json = new TextDecoder().decode(base64UrlDecode(parts[1]));\n return JSON.parse(json) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/** 解码 JWT header */\nexport function decodeJwtHeader(token: string): Record<string, unknown> | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n const json = new TextDecoder().decode(base64UrlDecode(parts[0]));\n return JSON.parse(json) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/** 标准化 issuer/base URL,去除末尾斜杠 */\nfunction normalizeIssuer(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\n/** OIDC Discovery 文档(仅取本模块关心的字段) */\nexport interface OidcDiscoveryDoc {\n issuer?: string;\n jwks_uri?: string;\n}\n\nlet cachedJwks: { baseUrl: string; jwks: Jwks; fetchedAt: number } | null = null;\nlet cachedDiscovery: { baseUrl: string; doc: OidcDiscoveryDoc; fetchedAt: number } | null = null;\nconst JWKS_CACHE_TTL_MS = 5 * 60 * 1000;\n/** discovery / JWKS 拉取超时(与 SsoClient._getDiscovery 的 10s 一致) */\nconst FETCH_TIMEOUT_MS = 10000;\n\n/**\n * 拉取 OIDC Discovery 文档(带内存缓存;失败返回 null 不抛错)\n *\n * 只缓存成功结果:失败(网络错误 / 非 2xx)不缓存,主站短暂抖动不会让\n * 登录持续失败到 TTL 期满,下一次调用立即重试(与 core/discovery.ts 策略一致)。\n */\nexport async function fetchDiscoveryDoc(baseUrl: string): Promise<OidcDiscoveryDoc | null> {\n const now = Date.now();\n if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {\n return cachedDiscovery.doc;\n }\n\n try {\n const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n });\n if (!res.ok) return null;\n const doc = (await res.json()) as OidcDiscoveryDoc;\n cachedDiscovery = { baseUrl, doc, fetchedAt: now };\n return doc;\n } catch {\n return null;\n }\n}\n\nexport interface FetchJwksOptions {\n /**\n * 绕过内存缓存与浏览器 HTTP 缓存(cache: \"no-cache\")强制重取 JWKS,\n * 并同步刷新模块级缓存。用于密钥轮换后 kid 失配的自愈重试。\n */\n forceRefresh?: boolean;\n}\n\n/** 从 SSO 中心拉取 JWKS(带内存缓存;优先 Discovery 的 jwks_uri,回退硬编码路径) */\nexport async function fetchJwks(baseUrl: string, options: FetchJwksOptions = {}): Promise<Jwks | null> {\n const now = Date.now();\n if (!options.forceRefresh && cachedJwks && cachedJwks.baseUrl === baseUrl && now - cachedJwks.fetchedAt < JWKS_CACHE_TTL_MS) {\n return cachedJwks.jwks;\n }\n\n const discovery = await fetchDiscoveryDoc(baseUrl);\n const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;\n\n try {\n const res = await fetch(jwksUri, {\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n ...(options.forceRefresh ? { cache: \"no-cache\" as RequestCache } : {}),\n });\n if (!res.ok) return null;\n const jwks = (await res.json()) as Jwks;\n cachedJwks = { baseUrl, jwks, fetchedAt: now };\n return jwks;\n } catch {\n return null;\n }\n}\n\n/** 清空 JWKS / Discovery 内存缓存(测试与密钥轮换场景使用) */\nexport function clearIdTokenCaches(): void {\n cachedJwks = null;\n cachedDiscovery = null;\n}\n\n/** 使用 RS256 公钥验证 ID Token 签名 */\nexport async function verifyRs256Signature(token: string, jwk: JwksKey): Promise<boolean> {\n try {\n const [headerB64, payloadB64, signature] = token.split(\".\");\n if (!signature || !jwk.n || !jwk.e) return false;\n\n const c = getCrypto();\n if (!c) return false;\n\n const cryptoKey = await c.subtle.importKey(\n \"jwk\",\n { kty: \"RSA\", n: jwk.n, e: jwk.e, alg: \"RS256\", ext: false },\n { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" },\n false,\n [\"verify\"]\n );\n\n const signatureBytes = base64UrlDecode(signature);\n const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);\n return await c.subtle.verify(\n \"RSASSA-PKCS1-v1_5\",\n cryptoKey,\n signatureBytes as unknown as BufferSource,\n data as unknown as BufferSource\n );\n } catch {\n return false;\n }\n}\n\n/** 计算 OIDC at_hash(access_token SHA-256 前 128bit base64url) */\nexport async function computeAtHash(accessToken: string): Promise<string> {\n const c = getCrypto();\n if (!c) return \"\";\n const hash = await c.subtle.digest(\"SHA-256\", new TextEncoder().encode(accessToken));\n const bytes = new Uint8Array(hash);\n const half = bytes.slice(0, bytes.length / 2);\n let binary = \"\";\n for (const b of half) {\n binary += String.fromCharCode(b);\n }\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/** 验证 ID Token(iss、aud、exp、iat、azp、sub、签名、at_hash) */\nexport async function validateIdToken(\n idToken: string,\n accessToken: string,\n expectedIssuer: string,\n expectedClientId: string,\n options: ValidateIdTokenOptions = {}\n): Promise<ValidateIdTokenResult> {\n const { rejectHs256WhenRs256Available = true, expectedNonce } = options;\n\n const header = decodeJwtHeader(idToken);\n if (!header) {\n throw new SsoError(\"id_token_invalid\", \"ID Token 格式错误\");\n }\n\n const alg = header.alg;\n if (typeof alg !== \"string\" || (alg !== \"RS256\" && alg !== \"HS256\")) {\n throw new SsoError(\"id_token_unsupported_alg\", `不支持的 ID Token 签名算法: ${alg}`);\n }\n\n const baseUrl = normalizeIssuer(expectedIssuer);\n // issuer 以 Discovery 文档的 issuer 为准(防伪站伪造 iss 通过校验),回退调用方传入值\n const discovery = await fetchDiscoveryDoc(baseUrl);\n const normalizedIssuer = normalizeIssuer(discovery?.issuer || baseUrl);\n\n // RS256:通过 JWKS 验证签名\n if (alg === \"RS256\") {\n const jwks = await fetchJwks(baseUrl);\n if (!jwks) {\n throw new SsoError(\"id_token_invalid_signature\", \"无法获取 JWKS 验证 ID Token 签名\");\n }\n const kid = typeof header.kid === \"string\" ? header.kid : undefined;\n // 有 kid 时精确匹配;无 kid 时逐个尝试所有 RS256 签名公钥\n const matchCandidates = (set: Jwks) =>\n set.keys.filter(\n (k) =>\n k.kty === \"RSA\" &&\n k.alg === \"RS256\" &&\n k.use === \"sig\" &&\n (kid ? k.kid === kid : true)\n );\n const verifyAny = async (keys: JwksKey[]): Promise<boolean> => {\n for (const key of keys) {\n if (await verifyRs256Signature(idToken, key)) return true;\n }\n return false;\n };\n\n let candidates = matchCandidates(jwks);\n let validSig = candidates.length > 0 ? await verifyAny(candidates) : false;\n\n // 密钥轮换自愈:按 kid 过滤无候选,或所有候选验签失败时,\n // 可能是本地 JWKS 缓存陈旧 —— 强制重取一次 JWKS 后重试,仍失败才抛错\n if (candidates.length === 0 || !validSig) {\n const freshJwks = await fetchJwks(baseUrl, { forceRefresh: true });\n if (freshJwks) {\n candidates = matchCandidates(freshJwks);\n validSig = candidates.length > 0 ? await verifyAny(candidates) : false;\n }\n }\n\n if (candidates.length === 0) {\n throw new SsoError(\"id_token_invalid_signature\", \"JWKS 中未找到匹配的 RS256 公钥\");\n }\n if (!validSig) {\n throw new SsoError(\"id_token_invalid_signature\", \"ID Token 签名验证失败\");\n }\n }\n\n // HS256:对称密钥无法安全分发给 Public Client / BFF。\n // SDK 拒绝 HS256 ID Token:主站需配置 RS256 密钥对以启用安全验证。\n if (alg === \"HS256\") {\n if (rejectHs256WhenRs256Available) {\n const jwks = await fetchJwks(baseUrl);\n const hasRs256 = jwks?.keys?.some((k) => k.alg === \"RS256\" && k.use === \"sig\");\n if (hasRs256) {\n throw new SsoError(\"id_token_unsupported_alg\", \"SSO 中心已配置 RS256,拒绝 HS256 ID Token\");\n }\n }\n throw new SsoError(\n \"id_token_hs256_unsupported\",\n \"ID Token 使用 HS256 对称签名,SDK 无法安全验证。\" +\n \"请联系管理员在 SSO 主站配置 RS256 密钥对(JWT_ID_TOKEN_PRIVATE_KEY / JWT_ID_TOKEN_PUBLIC_KEY)后重新签发。\"\n );\n }\n\n const payload = decodeJwtPayload(idToken);\n if (!payload) {\n throw new SsoError(\"id_token_invalid\", \"ID Token payload 解析失败\");\n }\n\n const tokenIssuer = typeof payload.iss === \"string\" ? normalizeIssuer(payload.iss) : \"\";\n if (tokenIssuer !== normalizedIssuer) {\n throw new SsoError(\"id_token_issuer_mismatch\", \"ID Token issuer 不匹配\");\n }\n\n // aud 校验:单值或数组包含 clientId;多 aud 时按 OIDC Core §3.1.3.7 要求 azp 必须等于 clientId\n const aud = payload.aud;\n const audList = Array.isArray(aud) ? aud : typeof aud === \"string\" ? [aud] : [];\n if (!audList.includes(expectedClientId)) {\n throw new SsoError(\"id_token_audience_mismatch\", \"ID Token audience 不匹配\");\n }\n if (audList.length > 1 && payload.azp !== expectedClientId) {\n throw new SsoError(\"id_token_audience_mismatch\", \"ID Token 多 audience 时 azp 必须为当前 client\");\n }\n\n // exp 必需且不得过期(允许 60s clock skew,与 iat 校验对称)\n if (typeof payload.exp !== \"number\") {\n throw new SsoError(\"id_token_invalid\", \"ID Token 缺少 exp 声明\");\n }\n if (Date.now() >= payload.exp * 1000 + 60_000) {\n throw new SsoError(\"id_token_expired\", \"ID Token 已过期\");\n }\n\n // iat 不得在未来(允许 60s clock skew)\n if (typeof payload.iat === \"number\" && payload.iat * 1000 > Date.now() + 60_000) {\n throw new SsoError(\"id_token_invalid\", \"ID Token iat 在未来,疑似伪造或时钟异常\");\n }\n\n if (typeof payload.sub !== \"string\" || !payload.sub) {\n throw new SsoError(\"id_token_missing_sub\", \"ID Token 缺少 sub\");\n }\n\n // 校验 at_hash:确保 ID Token 与当前 access_token 绑定(常量时间比较)\n if (typeof payload.at_hash === \"string\" && payload.at_hash) {\n const actual = await computeAtHash(accessToken);\n if (!timingSafeEqualString(actual, payload.at_hash)) {\n throw new SsoError(\"id_token_at_hash_mismatch\", \"ID Token at_hash 不匹配\");\n }\n }\n\n // 校验 nonce(OIDC Core §3.1.3.7):调用方传入 expectedNonce 时 fail-closed,\n // ID Token 必须携带 nonce 且与登录时生成的值常量时间相等,防 ID Token 重放\n if (expectedNonce !== undefined) {\n const tokenNonce = typeof payload.nonce === \"string\" ? payload.nonce : \"\";\n if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {\n throw new SsoError(\"id_token_nonce_mismatch\", \"ID Token nonce 不匹配\");\n }\n }\n\n return { sub: payload.sub };\n}\n","/**\r\n * SSO Client 核心类\r\n *\r\n * 封装完整的 OAuth 2.0 授权码 + PKCE S256 流程:\r\n * - login(): 发起授权请求\r\n * - handleCallback(): 处理回调,交换 token\r\n * - refreshToken(): 刷新 access token(原子轮换)\r\n * - getUserInfo(): 获取用户信息\r\n * - logout(): 登出\r\n * - isAuthenticated(): 检查认证状态\r\n */\r\n\r\nimport { SsoError, mapOAuthErrorToSsoCode } from \"./errors\";\r\nimport {\r\n generateCodeVerifier,\r\n generateCodeChallenge,\r\n generateState,\r\n} from \"./pkce\";\r\nimport {\r\n saveTokenData,\r\n getTokenData,\r\n removeTokenData,\r\n savePkceVerifier,\r\n getPkceVerifier,\r\n removePkceVerifier,\r\n saveOAuthState,\r\n getOAuthState,\r\n removeOAuthState,\r\n saveOAuthNonce,\r\n getOAuthNonce,\r\n removeOAuthNonce,\r\n saveLogoutState,\r\n getLogoutState,\r\n removeLogoutState,\r\n saveReturnUrl,\r\n saveSilentProbe,\r\n getSilentProbe,\r\n removeSilentProbe,\r\n clearAllSsoData,\r\n type TokenData,\r\n} from \"./storage\";\r\nimport { validateIdToken } from \"./id-token\";\r\nimport { isTrustedReturnUrl, timingSafeEqualString } from \"./security\";\r\n\r\n// ============================================\r\n// 类型定义\r\n// ============================================\r\n\r\n/** SSO 客户端配置 */\r\nexport interface SsoClientConfig {\r\n /** OAuth Client ID(从管理后台获取) */\r\n clientId: string;\r\n\r\n /** OAuth Client Secret(从管理后台获取)\r\n *\r\n * ⚠️ 安全提醒:client_secret 不应暴露在前端代码中。\r\n * 对于浏览器端 SPA(Public Client),应省略此字段;SDK 会使用 PKCE 完成授权,\r\n * 且 token 端点不会发送 client_secret。\r\n * 对于 BFF/Next.js 等 Confidential Client,可传入 client_secret。\r\n */\r\n clientSecret?: string;\r\n\r\n /** 回调 URL(必须与注册的 redirect_uri 完全一致) */\r\n redirectUri: string;\r\n\r\n /** SSO 中心地址,如 \"https://nihplod.cn\" */\r\n ssoBaseUrl: string;\r\n\r\n /** 请求的 scope(空格分隔),如 \"openid profile phone\" */\r\n scopes?: string;\r\n\r\n /** RP-Initiated Logout 返回地址(可选)。不传时回退到 redirectUri */\r\n postLogoutRedirectUri?: string;\r\n\r\n /**\r\n * 服务端到服务端调用的内网地址(可选,如 http://127.0.0.1:3000)。\r\n * 仅用于 discovery / token / userinfo / revocation 等服务器间请求;\r\n * 浏览器跳转(authorize、end-session)始终使用 ssoBaseUrl 公网地址。\r\n * 仅在 BFF/服务端场景配置;浏览器端(Public Client)不要配置。\r\n */\r\n serverBaseUrl?: string;\r\n}\r\n\r\n/** 用户信息 */\r\nexport interface SsoUser {\r\n sub: string;\r\n nickname?: string;\r\n avatar?: string;\r\n /** 手机号(脱敏,兼容保留的非标准 claim,新代码请使用 phone_number) */\r\n phone?: string;\r\n /** 手机号(脱敏,OIDC 标准 claim,与 phone 内容一致) */\r\n phone_number?: string;\r\n /** 生日(ISO 8601,需 birthday scope,未设置时为 null) */\r\n birthday?: string | null;\r\n membership_level?: string;\r\n /** 累计消费金额(单位:元,需 membership scope,与 membership_level 同源下发) */\r\n total_spent?: number;\r\n total_points?: number;\r\n}\r\n\r\n/** login() / getLoginUrl() 选项 */\r\nexport interface LoginOptions {\r\n /**\r\n * OIDC prompt 参数:\r\n * - \"none\":静默探测 SSO 会话。用户在 IdP 有会话时直接回跳完成登录;\r\n * 无会话时 IdP 回跳 error=login_required(或 consent_required /\r\n * interaction_required),handleCallback() 识别后返回 null 而不抛错\r\n * (CallbackPage 默认跳回 returnUrl 并附 sso_probe=no_session)。\r\n * ⚠️ 不要在页面加载时自动探测(会打扰无会话用户并消耗请求),\r\n * 应由用户点击等明确动作触发。\r\n * - \"login\":强制重新认证;\r\n * - \"consent\":强制重新授权确认。\r\n */\r\n prompt?: \"none\" | \"login\" | \"consent\";\r\n}\r\n\r\n/** prompt=none 静默探测失败时 IdP 回跳的 error 值(OIDC Core §3.1.2.6) */\r\nconst SILENT_PROBE_ERRORS = new Set([\r\n \"login_required\",\r\n \"consent_required\",\r\n \"interaction_required\",\r\n]);\r\n\r\n/** Token 响应 */\r\nexport interface TokenResponse {\r\n access_token: string;\r\n token_type: string;\r\n expires_in: number;\r\n refresh_token: string;\r\n id_token?: string;\r\n}\r\n\r\n/** OIDC Discovery 文档 */\r\nexport interface OidcDiscovery {\r\n issuer: string;\r\n authorization_endpoint: string;\r\n token_endpoint: string;\r\n userinfo_endpoint: string;\r\n jwks_uri: string;\r\n introspection_endpoint: string;\r\n revocation_endpoint?: string;\r\n end_session_endpoint?: string;\r\n scopes_supported: string[];\r\n response_types_supported: string[];\r\n grant_types_supported: string[];\r\n code_challenge_methods_supported: string[];\r\n}\r\n\r\n// ============================================\r\n// SsoClient\r\n// ============================================\r\n\r\nexport class SsoClient {\r\n public readonly config: SsoClientConfig;\r\n /** 服务器间调用的基准地址(未配置 serverBaseUrl 时等于 ssoBaseUrl) */\r\n private readonly _serverBase: string;\r\n private _discovery: OidcDiscovery | null = null;\r\n private _discoveryFetchedAt: number = 0;\r\n private _refreshLock: Promise<TokenData> | null = null;\r\n\r\n /** Discovery 文档缓存 TTL(5 分钟) */\r\n private static readonly DISCOVERY_TTL_MS = 5 * 60 * 1000;\r\n\r\n /** Discovery fetch 超时(10 秒) */\r\n private static readonly DISCOVERY_TIMEOUT_MS = 10000;\r\n\r\n constructor(config: SsoClientConfig) {\r\n if (!config.clientId) throw new SsoError(\"invalid_config\", \"clientId 不能为空\");\r\n if (!config.redirectUri) throw new SsoError(\"invalid_config\", \"redirectUri 不能为空\");\r\n if (!config.ssoBaseUrl) throw new SsoError(\"invalid_config\", \"ssoBaseUrl 不能为空\");\r\n\r\n // 规范化 ssoBaseUrl:移除末尾斜杠\r\n const base = config.ssoBaseUrl.replace(/\\/+$/, \"\");\r\n this.config = { ...config, ssoBaseUrl: base };\r\n this._serverBase = (config.serverBaseUrl ?? base).replace(/\\/+$/, \"\");\r\n }\r\n\r\n // ============================================\r\n // 内部方法\r\n // ============================================\r\n\r\n /**\r\n * 获取 OIDC Discovery 文档(带缓存 + 超时)\r\n *\r\n * 缓存 5 分钟,超时 10 秒。\r\n * 失败时返回 null(上层调用方回退到硬编码默认端点)。\r\n */\r\n private async _getDiscovery(): Promise<OidcDiscovery | null> {\r\n const now = Date.now();\r\n\r\n // 命中缓存且未过期\r\n if (\r\n this._discovery &&\r\n now - this._discoveryFetchedAt < SsoClient.DISCOVERY_TTL_MS\r\n ) {\r\n return this._discovery;\r\n }\r\n\r\n // discovery 是服务器间调用:配置了 serverBaseUrl 时经内网拉取\r\n // (文档内容由 SSO 中心按公网 origin 生成,与拉取通道无关)\r\n const url = `${this._serverBase}/api/oauth/.well-known/openid-configuration`;\r\n\r\n try {\r\n const controller = new AbortController();\r\n const timeoutId = setTimeout(\r\n () => controller.abort(),\r\n SsoClient.DISCOVERY_TIMEOUT_MS\r\n );\r\n\r\n const res = await fetch(url, { signal: controller.signal });\r\n clearTimeout(timeoutId);\r\n\r\n if (!res.ok) {\r\n console.warn(\r\n `[SSO SDK] OIDC Discovery 请求失败: HTTP ${res.status}, 回退到默认端点`\r\n );\r\n return null;\r\n }\r\n\r\n this._discovery = (await res.json()) as OidcDiscovery;\r\n this._discoveryFetchedAt = now;\r\n return this._discovery;\r\n } catch (err) {\r\n console.warn(\r\n `[SSO SDK] OIDC Discovery 请求异常: ${err instanceof Error ? err.message : String(err)}, 回退到默认端点`\r\n );\r\n // 不清空已有缓存(即使过期也优于无数据)\r\n return this._discovery || null;\r\n }\r\n }\r\n\r\n /** 获取 authorize 端点 URL(优先 Discovery,回退默认) */\r\n private async _getAuthorizeEndpoint(): Promise<string> {\r\n const d = await this._getDiscovery();\r\n if (d) return d.authorization_endpoint;\r\n return `${this.config.ssoBaseUrl}/api/oauth/authorize`;\r\n }\r\n\r\n /** 获取 token 端点 URL(服务器间调用:serverBaseUrl 直连优先,其次 Discovery,回退默认) */\r\n private async _getTokenEndpoint(): Promise<string> {\r\n if (this.config.serverBaseUrl) return `${this._serverBase}/api/oauth/token`;\r\n const d = await this._getDiscovery();\r\n if (d) return d.token_endpoint;\r\n return `${this.config.ssoBaseUrl}/api/oauth/token`;\r\n }\r\n\r\n /** 获取 userinfo 端点 URL(服务器间调用:serverBaseUrl 直连优先,其次 Discovery,回退默认) */\r\n private async _getUserinfoEndpoint(): Promise<string> {\r\n if (this.config.serverBaseUrl) return `${this._serverBase}/api/oauth/userinfo`;\r\n const d = await this._getDiscovery();\r\n if (d) return d.userinfo_endpoint;\r\n return `${this.config.ssoBaseUrl}/api/oauth/userinfo`;\r\n }\r\n\r\n /**\r\n * 开放重定向防护:仅保存相对路径或与当前页面同源的 returnUrl,\r\n * 其余(如 https://evil.com)忽略并告警,防止回调后跳转到钓鱼站点。\r\n */\r\n private _saveReturnUrlIfTrusted(returnUrl: string): void {\r\n if (isTrustedReturnUrl(returnUrl, window.location.origin)) {\r\n // 按 clientId 隔离存储,与 CallbackPage 读取的 key 对应\r\n saveReturnUrl(returnUrl, this.config.clientId);\r\n } else {\r\n console.warn(`[SSO SDK] returnUrl 未通过同源校验,已忽略: ${returnUrl}`);\r\n }\r\n }\r\n\r\n // ============================================\r\n // 公共 API\r\n // ============================================\r\n\r\n /**\r\n * 发起 SSO 登录\r\n *\r\n * 生成 PKCE code_verifier/code_challenge 和 state 参数,\r\n * 构建 authorize URL,通过 302 跳转到 SSO 登录页。\r\n *\r\n * @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage;\r\n * 仅允许相对路径或同源绝对 URL,否则忽略并告警)\r\n * @param options.prompt - OIDC prompt 参数。传 \"none\" 为静默探测:\r\n * 额外在存储中记录探测标记(该次请求的 state),回调无会话时\r\n * handleCallback() 返回 null 而不抛错。\r\n *\r\n * ⚠️ 不要与 getLoginUrl() 混用:两者都会重新生成并覆盖 sessionStorage 中的\r\n * state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。\r\n * 同一次登录只使用其中一个入口。\r\n */\r\n async login(returnUrl?: string, options: LoginOptions = {}): Promise<void> {\r\n const verifier = generateCodeVerifier();\r\n const challenge = await generateCodeChallenge(verifier);\r\n const state = generateState();\r\n // OIDC nonce(256-bit 随机):绑定 ID Token 到本次登录会话,防重放\r\n const nonce = generateState();\r\n\r\n // 保存 PKCE verifier、state 和 nonce 到 sessionStorage(整页重定向后仍可读取)\r\n savePkceVerifier(this.config.clientId, verifier);\r\n saveOAuthState(state, this.config.clientId);\r\n saveOAuthNonce(nonce, this.config.clientId);\r\n // 静默探测标记:存该次请求的 state,回调凭它识别\"探测无会话\"\r\n if (options.prompt === \"none\") {\r\n saveSilentProbe(state, this.config.clientId);\r\n } else {\r\n removeSilentProbe(this.config.clientId);\r\n }\r\n\r\n if (returnUrl) {\r\n this._saveReturnUrlIfTrusted(returnUrl);\r\n }\r\n\r\n // 构建 authorize URL\r\n const authorizeEndpoint = await this._getAuthorizeEndpoint();\r\n const params = new URLSearchParams();\r\n params.set(\"response_type\", \"code\");\r\n params.set(\"client_id\", this.config.clientId);\r\n params.set(\"redirect_uri\", this.config.redirectUri);\r\n params.set(\"scope\", this.config.scopes || \"openid profile\");\r\n params.set(\"state\", state);\r\n params.set(\"nonce\", nonce);\r\n params.set(\"code_challenge\", challenge);\r\n params.set(\"code_challenge_method\", \"S256\");\r\n if (options.prompt) {\r\n params.set(\"prompt\", options.prompt);\r\n }\r\n\r\n // 跳转\r\n window.location.href = `${authorizeEndpoint}?${params.toString()}`;\r\n }\r\n\r\n /**\r\n * 构建登录 URL(不跳转,返回 URL 字符串)\r\n *\r\n * 适用于需要手动处理跳转的场景。\r\n *\r\n * ⚠️ 不要与 login() 混用:两者都会重新生成并覆盖 sessionStorage 中的\r\n * state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。\r\n * 同一次登录只使用其中一个入口。\r\n */\r\n async getLoginUrl(returnUrl?: string, options: LoginOptions = {}): Promise<string> {\r\n const verifier = generateCodeVerifier();\r\n const challenge = await generateCodeChallenge(verifier);\r\n const state = generateState();\r\n // OIDC nonce(与 state 同等强度的随机值),回调时校验 ID Token 的 nonce claim\r\n const nonce = generateState();\r\n\r\n savePkceVerifier(this.config.clientId, verifier);\r\n saveOAuthState(state, this.config.clientId);\r\n saveOAuthNonce(nonce, this.config.clientId);\r\n // 静默探测标记(与 login() 一致):prompt=none 时记录该次请求的 state\r\n if (options.prompt === \"none\") {\r\n saveSilentProbe(state, this.config.clientId);\r\n } else {\r\n removeSilentProbe(this.config.clientId);\r\n }\r\n // returnUrl 按 clientId 隔离,与 login() / CallbackPage 保持一致\r\n if (returnUrl) this._saveReturnUrlIfTrusted(returnUrl);\r\n\r\n const authorizeEndpoint = await this._getAuthorizeEndpoint();\r\n const params = new URLSearchParams();\r\n params.set(\"response_type\", \"code\");\r\n params.set(\"client_id\", this.config.clientId);\r\n params.set(\"redirect_uri\", this.config.redirectUri);\r\n params.set(\"scope\", this.config.scopes || \"openid profile\");\r\n params.set(\"state\", state);\r\n params.set(\"nonce\", nonce);\r\n params.set(\"code_challenge\", challenge);\r\n params.set(\"code_challenge_method\", \"S256\");\r\n if (options.prompt) {\r\n params.set(\"prompt\", options.prompt);\r\n }\r\n\r\n return `${authorizeEndpoint}?${params.toString()}`;\r\n }\r\n\r\n /**\r\n * 弹窗模式 SSO 登录\r\n *\r\n * 打开一个小窗口进行登录,认证完成后窗口自动关闭,\r\n * 主页面不丢失状态(适用于 SPA 中需要保持表单/浏览上下文的场景)。\r\n *\r\n * 流程:\r\n * 1. window.open() 打开授权 URL 到弹窗\r\n * 2. 用户在弹窗中完成登录\r\n * 3. 弹窗加载 CallbackPage 时检测到 window.opener,通过 postMessage 回传回调 URL\r\n * 4. 主页面收到消息后调用 handleCallback() 交换 token\r\n * 5. 弹窗自动关闭\r\n *\r\n * @param options - 弹窗配置\r\n * @param options.returnUrl - 登录成功后的返回地址\r\n * @param options.width - 弹窗宽度(默认 480)\r\n * @param options.height - 弹窗高度(默认 640)\r\n * @returns TokenData\r\n *\r\n * @example\r\n * ```typescript\r\n * const client = new SsoClient({ clientId: \"xxx\", redirectUri: \"https://myapp.com/callback\", ssoBaseUrl: \"https://nihplod.cn\" });\r\n * try {\r\n * const tokenData = await client.loginPopup({ returnUrl: \"/dashboard\" });\r\n * console.log(\"登录成功\", tokenData);\r\n * } catch (err) {\r\n * if (err instanceof SsoError && err.code === \"popup_blocked\") {\r\n * // 弹窗被拦截,回退到同页重定向\r\n * await client.login(\"/dashboard\");\r\n * }\r\n * }\r\n * ```\r\n */\r\n async loginPopup(\r\n options: { returnUrl?: string; width?: number; height?: number } = {}\r\n ): Promise<TokenData> {\r\n const verifier = generateCodeVerifier();\r\n const challenge = await generateCodeChallenge(verifier);\r\n const state = generateState();\r\n const nonce = generateState(); // OIDC nonce:回调时校验 ID Token 的 nonce claim\r\n const popupNonce = generateState(); // 独立的 nonce 用于 postMessage 来源验证\r\n\r\n savePkceVerifier(this.config.clientId, verifier);\r\n saveOAuthState(state, this.config.clientId);\r\n saveOAuthNonce(nonce, this.config.clientId);\r\n // 保存 popup nonce 到 sessionStorage,用于 postMessage 校验\r\n savePkceVerifier(`${this.config.clientId}_popup_nonce`, popupNonce);\r\n\r\n if (options.returnUrl) {\r\n this._saveReturnUrlIfTrusted(options.returnUrl);\r\n }\r\n\r\n const authorizeEndpoint = await this._getAuthorizeEndpoint();\r\n const params = new URLSearchParams();\r\n params.set(\"response_type\", \"code\");\r\n params.set(\"client_id\", this.config.clientId);\r\n params.set(\"redirect_uri\", this.config.redirectUri);\r\n params.set(\"scope\", this.config.scopes || \"openid profile\");\r\n params.set(\"state\", state);\r\n params.set(\"nonce\", nonce);\r\n params.set(\"code_challenge\", challenge);\r\n params.set(\"code_challenge_method\", \"S256\");\r\n // 弹窗 nonce:回调页通过 postMessage 回传,主窗口校验防伪造\r\n params.set(\"popup_nonce\", popupNonce);\r\n\r\n const width = options.width || 480;\r\n const height = options.height || 640;\r\n const left = Math.max(0, (window.screen.width - width) / 2);\r\n const top = Math.max(0, (window.screen.height - height) / 2);\r\n\r\n const popupFeatures = [\r\n `width=${width}`,\r\n `height=${height}`,\r\n `left=${Math.round(left)}`,\r\n `top=${Math.round(top)}`,\r\n \"resizable=yes\",\r\n \"scrollbars=yes\",\r\n \"status=yes\",\r\n ].join(\",\");\r\n\r\n const popup = window.open(\r\n `${authorizeEndpoint}?${params.toString()}`,\r\n \"nihplod_sso_popup\",\r\n popupFeatures\r\n );\r\n\r\n if (!popup) {\r\n removePkceVerifier(this.config.clientId);\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n throw new SsoError(\r\n \"popup_blocked\",\r\n \"弹窗被浏览器拦截,请允许弹窗后重试\"\r\n );\r\n }\r\n\r\n // 计算 redirect_uri 的 origin,用于 postMessage 来源校验\r\n const redirectUriOrigin = new URL(this.config.redirectUri).origin;\r\n\r\n try {\r\n // 聚焦弹窗\r\n popup.focus();\r\n\r\n return await new Promise<TokenData>((resolve, reject) => {\r\n let completed = false;\r\n\r\n const handleMessage = (event: MessageEvent) => {\r\n if (completed) return;\r\n // 校验消息来源:必须来自 redirect_uri 对应的子项目 origin(回调页运行位置)\r\n if (event.origin !== redirectUriOrigin) return;\r\n if (!event.data || event.data.type !== \"nihplod_sso_popup_callback\") return;\r\n if (!event.data.callbackUrl) return;\r\n\r\n // 校验 popup nonce:防止伪造的 postMessage(fail-closed:本地 nonce 缺失同样拒绝)\r\n const savedNonce = getPkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n if (!savedNonce || !timingSafeEqualString(event.data.nonce ?? \"\", savedNonce)) return;\r\n removePkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n\r\n // 回 ACK:回调页会每 500ms 重发直到收到 ACK(或超时提示手动关闭),\r\n // 防止主窗口监听未挂载时消息丢失导致弹窗悬挂\r\n try {\r\n (event.source as Window | null)?.postMessage(\r\n { type: \"nihplod_sso_popup_ack\", nonce: savedNonce },\r\n event.origin\r\n );\r\n } catch {\r\n // ACK 失败不影响主流程(弹窗会超时提示手动关闭)\r\n }\r\n\r\n completed = true;\r\n cleanup();\r\n\r\n if (popup && !popup.closed) {\r\n popup.close();\r\n }\r\n\r\n this.handleCallback(event.data.callbackUrl)\r\n .then((tokenData) => {\r\n // prompt=none 静默探测在弹窗模式不支持(loginPopup 不带 prompt),\r\n // null 结果视为探测无会话\r\n if (tokenData === null) {\r\n reject(\r\n new SsoError(\"not_authenticated\", \"SSO 会话不存在(静默探测无会话)\")\r\n );\r\n return;\r\n }\r\n resolve(tokenData);\r\n })\r\n .catch(reject);\r\n };\r\n\r\n const pollTimer = setInterval(() => {\r\n if (popup.closed) {\r\n if (!completed) {\r\n completed = true;\r\n cleanup();\r\n reject(\r\n new SsoError(\r\n \"popup_closed\",\r\n \"登录窗口已关闭\"\r\n )\r\n );\r\n }\r\n }\r\n }, 500);\r\n\r\n const cleanup = () => {\r\n clearInterval(pollTimer);\r\n window.removeEventListener(\"message\", handleMessage);\r\n removePkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n };\r\n\r\n window.addEventListener(\"message\", handleMessage);\r\n });\r\n } catch (err) {\r\n removePkceVerifier(this.config.clientId);\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n throw err;\r\n }\r\n }\r\n\r\n /**\r\n * 处理 OAuth 回调\r\n *\r\n * 解析回调 URL,校验 state 参数,用授权码交换 token。\r\n * 成功后 token 自动保存到 token 存储(默认 sessionStorage,可通过 setTokenStorage 定制)。\r\n *\r\n * @param callbackUrl - 完整的回调 URL(window.location.href)\r\n * @returns TokenData;prompt=none 静默探测且无 SSO 会话时返回 null\r\n * (IdP 回跳 login_required / consent_required / interaction_required\r\n * 且回调 state 与探测标记匹配),此时临时数据已清理,调用方按\"未登录\"处理即可\r\n */\r\n async handleCallback(\r\n callbackUrl: string\r\n ): Promise<TokenData | null> {\r\n const url = new URL(callbackUrl);\r\n const params = url.searchParams;\r\n\r\n // 检查错误(SSO 中心按 OAuth 2.0 规范回传 error 参数)\r\n const error = params.get(\"error\");\r\n if (error) {\r\n // 静默探测(prompt=none)无会话:IdP 以 error 回跳属预期结果而非异常。\r\n // 要求回调 state 与登录 state、探测标记三者一致(常量时间比较),\r\n // 防止伪造的 error 回调被当作探测结果\r\n const returnedErrorState = params.get(\"state\");\r\n const probeState = getSilentProbe(this.config.clientId);\r\n const savedStateForProbe = getOAuthState(this.config.clientId);\r\n if (\r\n SILENT_PROBE_ERRORS.has(error) &&\r\n probeState &&\r\n savedStateForProbe &&\r\n timingSafeEqualString(savedStateForProbe, returnedErrorState ?? \"\") &&\r\n timingSafeEqualString(probeState, returnedErrorState ?? \"\")\r\n ) {\r\n // 清除本次探测的全部临时数据(state/nonce/verifier/标记),返回 null\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n removeSilentProbe(this.config.clientId);\r\n return null;\r\n }\r\n // 授权已失败,本次流程的临时数据(state/verifier/nonce/探测标记)不再有用,一并清理避免残留\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n removeSilentProbe(this.config.clientId);\r\n const desc = params.get(\"error_description\") || error;\r\n throw new SsoError(mapOAuthErrorToSsoCode(error), `授权失败: ${desc}`);\r\n }\r\n\r\n const code = params.get(\"code\");\r\n const returnedState = params.get(\"state\");\r\n\r\n if (!code) {\r\n // 缺 code 的回调无法继续,清理临时数据避免残留\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n removeSilentProbe(this.config.clientId);\r\n throw new SsoError(\"token_request_failed\", \"回调 URL 中缺少 authorization code\");\r\n }\r\n\r\n // 校验 state 参数(CSRF 防护,常量时间比较)\r\n const savedState = getOAuthState(this.config.clientId);\r\n if (!savedState || !timingSafeEqualString(savedState, returnedState ?? \"\")) {\r\n // state 不匹配:state 与 verifier 一并清除,避免残留半套临时数据\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n removeSilentProbe(this.config.clientId);\r\n throw new SsoError(\r\n \"state_mismatch\",\r\n \"State 参数不匹配,可能存在 CSRF 攻击\"\r\n );\r\n }\r\n\r\n // state 校验通过:读取登录时保存的 OIDC nonce(validateIdToken 时 fail-closed 校验,\r\n // 成功后才随 state/verifier 一并清除——验签失败重试仍需它)\r\n const expectedNonce = getOAuthNonce(this.config.clientId) ?? undefined;\r\n\r\n // 获取 code_verifier\r\n const verifier = getPkceVerifier(this.config.clientId);\r\n if (!verifier) {\r\n throw new SsoError(\r\n \"pkce_required\",\r\n \"code_verifier 不存在(可能已过期或来自其他标签页)\"\r\n );\r\n }\r\n\r\n // 交换 token\r\n const tokenEndpoint = await this._getTokenEndpoint();\r\n const body = new URLSearchParams();\r\n body.set(\"grant_type\", \"authorization_code\");\r\n body.set(\"code\", code);\r\n body.set(\"code_verifier\", verifier);\r\n body.set(\"client_id\", this.config.clientId);\r\n body.set(\"redirect_uri\", this.config.redirectUri);\r\n if (this.config.clientSecret) {\r\n body.set(\"client_secret\", this.config.clientSecret);\r\n }\r\n\r\n let res: Response;\r\n try {\r\n res = await fetch(tokenEndpoint, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: body.toString(),\r\n });\r\n } catch (err) {\r\n throw new SsoError(\"network_error\", \"网络请求失败\", err);\r\n }\r\n\r\n if (!res.ok) {\r\n let errData: Record<string, unknown> = {};\r\n try { errData = await res.json(); } catch { /* ignore */ }\r\n const serverError = (errData.error as string) || \"\";\r\n throw new SsoError(\r\n mapOAuthErrorToSsoCode(serverError),\r\n (errData.error_description as string) || `Token 请求失败: HTTP ${res.status}`\r\n );\r\n }\r\n\r\n // 先解析响应体,成功后再清除 state 和 verifier:\r\n // 若 JSON 畸形导致解析抛错,保留 state/verifier 允许用户重试回调\r\n const data: TokenResponse = await res.json();\r\n\r\n // OIDC:验证 ID Token 签名、基本声明及 at_hash\r\n if (data.id_token) {\r\n try {\r\n await validateIdToken(\r\n data.id_token,\r\n data.access_token,\r\n this.config.ssoBaseUrl,\r\n this.config.clientId,\r\n { expectedNonce }\r\n );\r\n } catch (err) {\r\n // 验证失败:不保存任何 token,防止伪造 ID Token\r\n removeTokenData(this.config.clientId);\r\n throw err;\r\n }\r\n }\r\n\r\n // 全部校验通过后清除 state、nonce 和 verifier(一次性临时数据)。\r\n // 推迟到验签之后:若 JWKS 暂时不可达导致验签失败,\r\n // 保留 state/verifier/nonce,用户刷新回调页即可重试\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n removeSilentProbe(this.config.clientId);\r\n\r\n const now = Date.now();\r\n const tokenData: TokenData = {\r\n access_token: data.access_token,\r\n token_type: data.token_type,\r\n expires_in: data.expires_in,\r\n refresh_token: data.refresh_token,\r\n id_token: data.id_token,\r\n issued_at: now,\r\n expires_at: now + data.expires_in * 1000,\r\n };\r\n\r\n saveTokenData(tokenData, this.config.clientId);\r\n return tokenData;\r\n }\r\n\r\n /**\r\n * 刷新 Access Token\r\n *\r\n * 使用 refresh_token 换取新的 access_token。\r\n * 采用互斥锁防止并发刷新。\r\n * 支持 Refresh Token 原子轮换。\r\n */\r\n async refreshToken(): Promise<TokenData> {\r\n // 互斥锁:避免并发刷新\r\n if (this._refreshLock) return this._refreshLock;\r\n\r\n this._refreshLock = this._doRefreshToken();\r\n try {\r\n return await this._refreshLock;\r\n } finally {\r\n this._refreshLock = null;\r\n }\r\n }\r\n\r\n private async _doRefreshToken(): Promise<TokenData> {\r\n const current = getTokenData(this.config.clientId);\r\n if (!current?.refresh_token) {\r\n throw new SsoError(\"no_refresh_token\", \"没有可用的 refresh_token\");\r\n }\r\n\r\n const tokenEndpoint = await this._getTokenEndpoint();\r\n const body = new URLSearchParams();\r\n body.set(\"grant_type\", \"refresh_token\");\r\n body.set(\"refresh_token\", current.refresh_token);\r\n body.set(\"client_id\", this.config.clientId);\r\n if (this.config.clientSecret) {\r\n body.set(\"client_secret\", this.config.clientSecret);\r\n }\r\n\r\n let res: Response | null = null;\r\n let lastErr: unknown;\r\n for (let attempt = 0; attempt < 2; attempt++) {\r\n try {\r\n res = await fetch(tokenEndpoint, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: body.toString(),\r\n });\r\n lastErr = null;\r\n break;\r\n } catch (err) {\r\n lastErr = err;\r\n if (attempt < 1) await new Promise((r) => setTimeout(r, 1000));\r\n }\r\n }\r\n if (lastErr || !res) {\r\n throw new SsoError(\"network_error\", \"刷新 Token 网络请求失败\", lastErr);\r\n }\r\n\r\n if (!res.ok) {\r\n let errData: Record<string, unknown> = {};\r\n try { errData = await res.json(); } catch { /* ignore */ }\r\n const errorCode = (errData.error as string) || \"\";\r\n // invalid_grant 表示 refresh_token 已被撤销或过期,应清除本地登录态\r\n if (errorCode === \"invalid_grant\" || res.status === 401) {\r\n removeTokenData(this.config.clientId);\r\n }\r\n throw new SsoError(\r\n mapOAuthErrorToSsoCode(errorCode, \"refresh\"),\r\n (errData.error_description as string) || `刷新 Token 失败: HTTP ${res.status}`\r\n );\r\n }\r\n\r\n const data: TokenResponse = await res.json();\r\n\r\n if (data.id_token) {\r\n try {\r\n await validateIdToken(\r\n data.id_token,\r\n data.access_token,\r\n this.config.ssoBaseUrl,\r\n this.config.clientId\r\n );\r\n } catch (err) {\r\n removeTokenData(this.config.clientId);\r\n throw err;\r\n }\r\n }\r\n\r\n const now = Date.now();\r\n const tokenData: TokenData = {\r\n access_token: data.access_token,\r\n token_type: data.token_type,\r\n expires_in: data.expires_in,\r\n // RFC 6749 §6:刷新响应可省略 refresh_token,此时沿用旧值\r\n refresh_token: data.refresh_token || current.refresh_token,\r\n id_token: data.id_token,\r\n issued_at: now,\r\n expires_at: now + data.expires_in * 1000,\r\n };\r\n\r\n saveTokenData(tokenData, this.config.clientId);\r\n return tokenData;\r\n }\r\n\r\n /**\r\n * 获取用户信息\r\n *\r\n * 若 access_token 已过期则自动刷新后再请求。\r\n */\r\n async getUserInfo(): Promise<SsoUser> {\r\n let tokenData = getTokenData(this.config.clientId);\r\n\r\n if (!tokenData) {\r\n throw new SsoError(\"not_authenticated\", \"未登录\");\r\n }\r\n\r\n // 若已过期,先刷新\r\n if (Date.now() >= tokenData.expires_at) {\r\n tokenData = await this.refreshToken();\r\n }\r\n\r\n const userinfoEndpoint = await this._getUserinfoEndpoint();\r\n\r\n let res: Response;\r\n try {\r\n res = await fetch(userinfoEndpoint, {\r\n headers: {\r\n Authorization: `Bearer ${tokenData.access_token}`,\r\n },\r\n });\r\n } catch (err) {\r\n throw new SsoError(\"network_error\", \"获取用户信息网络请求失败\", err);\r\n }\r\n\r\n if (!res.ok) {\r\n if (res.status === 401) {\r\n removeTokenData(this.config.clientId);\r\n throw new SsoError(\"not_authenticated\", \"Token 已失效\");\r\n }\r\n throw new SsoError(\r\n \"userinfo_failed\",\r\n `获取用户信息失败: HTTP ${res.status}`\r\n );\r\n }\r\n\r\n return (await res.json()) as SsoUser;\r\n }\r\n\r\n /**\r\n * 获取当前 access_token\r\n *\r\n * 若已过期则自动刷新。用于子项目自行发起 API 请求时获取 Bearer token。\r\n * 若无 token 返回 null;若刷新失败则抛出错误(与 getUserInfo 行为一致)。\r\n */\r\n async getAccessToken(): Promise<string | null> {\r\n let tokenData = getTokenData(this.config.clientId);\r\n if (!tokenData) return null;\r\n\r\n if (Date.now() >= tokenData.expires_at) {\r\n tokenData = await this.refreshToken();\r\n }\r\n\r\n return tokenData.access_token;\r\n }\r\n\r\n /**\r\n * 检查是否已认证(不发起网络请求)\r\n *\r\n * 仅检查本地是否存在未过期的 access_token。\r\n */\r\n isAuthenticated(): boolean {\r\n const tokenData = getTokenData(this.config.clientId);\r\n if (!tokenData) return false;\r\n return Date.now() < tokenData.expires_at;\r\n }\r\n\r\n /**\r\n * 登出\r\n *\r\n * 清除本地所有 token 和临时数据,并尝试撤销服务端 refresh_token。\r\n * @param redirectToSso - 是否重定向到 SSO 登出页(默认 false)。\r\n * 为 true 时携带 state 参数(已保存到 sessionStorage),\r\n * 回跳页面应调用 validateLogoutState() 校验防登出 CSRF。\r\n */\r\n async logout(redirectToSso: boolean = false): Promise<void> {\r\n // 获取 refresh_token 用于服务端撤销(需在 clearAllSsoData 之前)\r\n const tokenData = getTokenData(this.config.clientId);\r\n const refreshToken = tokenData?.refresh_token;\r\n const idTokenHint = tokenData?.id_token;\r\n\r\n clearAllSsoData(this.config.clientId);\r\n\r\n // 尝试调用服务端 token revocation 端点(best-effort)\r\n // 仅 Confidential Client 携带 client_secret;Public Client 不传 secret。\r\n if (refreshToken && this.config.clientId) {\r\n try {\r\n // serverBaseUrl 配置时直连内网默认端点(discovery 文档内是公网 URL,\r\n // 会绕行公网);否则用 Discovery,兜底公网默认路径\r\n const discovery = this.config.serverBaseUrl ? null : await this._getDiscovery();\r\n const revokeUrl = this.config.serverBaseUrl\r\n ? `${this._serverBase}/api/oauth/revoke`\r\n : discovery?.revocation_endpoint ||\r\n `${this.config.ssoBaseUrl}/api/oauth/revoke`;\r\n const revokeBody = new URLSearchParams({\r\n token: refreshToken,\r\n token_type_hint: \"refresh_token\",\r\n client_id: this.config.clientId,\r\n });\r\n if (this.config.clientSecret) {\r\n revokeBody.set(\"client_secret\", this.config.clientSecret);\r\n }\r\n await fetch(revokeUrl, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: revokeBody.toString(),\r\n });\r\n } catch {\r\n // 撤销失败不影响本地登出(best-effort)\r\n }\r\n }\r\n\r\n if (redirectToSso) {\r\n // OIDC RP-Initiated Logout:携带 client_id / post_logout_redirect_uri / id_token_hint / state\r\n const discovery = await this._getDiscovery();\r\n const endSessionEndpoint =\r\n discovery?.end_session_endpoint || `${this.config.ssoBaseUrl}/api/oauth/end-session`;\r\n const logoutUrl = new URL(endSessionEndpoint);\r\n logoutUrl.searchParams.set(\"client_id\", this.config.clientId);\r\n const postLogoutUri = this.config.postLogoutRedirectUri || this.config.redirectUri;\r\n if (postLogoutUri) {\r\n logoutUrl.searchParams.set(\r\n \"post_logout_redirect_uri\",\r\n postLogoutUri\r\n );\r\n }\r\n if (idTokenHint) {\r\n logoutUrl.searchParams.set(\"id_token_hint\", idTokenHint);\r\n }\r\n // 携带 state 防登出 CSRF:回跳时用 validateLogoutState() 校验\r\n const state = generateState();\r\n saveLogoutState(state, this.config.clientId);\r\n logoutUrl.searchParams.set(\"state\", state);\r\n window.location.href = logoutUrl.toString();\r\n }\r\n }\r\n\r\n /**\r\n * 校验 RP-Initiated Logout 回跳的 state 参数(登出 CSRF 防护)\r\n *\r\n * 在 post_logout_redirect_uri 指向的页面加载时调用;\r\n * 仅在 URL 携带 state 且与 logout(redirectToSso=true) 保存的值一致时返回 true,\r\n * 校验后清除已保存的 logout state(一次性)。\r\n *\r\n * @param url - 当前页面完整 URL(window.location.href)\r\n *\r\n * @example\r\n * ```typescript\r\n * if (sso.validateLogoutState(window.location.href)) {\r\n * // 来自 SSO 登出的可信回跳\r\n * }\r\n * ```\r\n */\r\n validateLogoutState(url: string): boolean {\r\n const returnedState = new URL(url).searchParams.get(\"state\");\r\n if (!returnedState) return false;\r\n const savedState = getLogoutState(this.config.clientId);\r\n if (!savedState) return false;\r\n if (!timingSafeEqualString(savedState, returnedState)) return false;\r\n // 校验通过:logout state 为一次性临时数据,立即清除\r\n removeLogoutState(this.config.clientId);\r\n return true;\r\n }\r\n\r\n /**\r\n * 获取 OIDC Discovery 文档\r\n *\r\n * 用于调试和获取 SSO 中心完整配置。\r\n * 可能返回 null(当 Discovery 端点不可达且无缓存时)。\r\n */\r\n async getDiscovery(): Promise<OidcDiscovery | null> {\r\n return this._getDiscovery();\r\n }\r\n}\r\n","/**\r\n * RequireAuth / withAuth — 路由保护组件\r\n *\r\n * 用法:\r\n * - <RequireAuth> 包裹需要登录才能访问的路由\r\n * - withAuth(Component) HOC 包装单个页面组件\r\n */\r\n\r\n\"use client\";\r\n\r\nimport React, { useEffect, useRef, useState, type ComponentType } from \"react\";\r\nimport { useSso } from \"./SsoProvider\";\r\n\r\n// ============================================\r\n// RequireAuth 组件\r\n// ============================================\r\n\r\nexport interface RequireAuthProps {\r\n /** 子组件(受保护的内容) */\r\n children: React.ReactNode;\r\n\r\n /** 未登录时的回退内容(默认显示加载中) */\r\n fallback?: React.ReactNode;\r\n\r\n /** 是否在检测到未登录时自动发起登录跳转 */\r\n autoLogin?: boolean;\r\n\r\n /**\r\n * 是否使用弹窗模式登录(保持当前页面状态不丢失)\r\n *\r\n * 仅在 autoLogin=true 时生效。弹窗被拦截时自动回退到同页重定向。\r\n */\r\n usePopup?: boolean;\r\n\r\n /**\r\n * 登录发起失败时的回调(如弹窗被关闭 popup_closed)。\r\n * 失败后组件会展示重试入口,不会永久停在\"请先登录\"。\r\n */\r\n onError?: (error: unknown) => void;\r\n\r\n /**\r\n * 自定义登录失败 UI。不传时显示默认的错误提示与\"重试\"按钮。\r\n * retry() 会重新发起登录。\r\n */\r\n renderLoginError?: (error: unknown, retry: () => void) => React.ReactNode;\r\n}\r\n\r\n/**\r\n * 路由保护组件\r\n *\r\n * 未登录时自动触发登录跳转或显示回退内容。\r\n * 登录发起失败(如弹窗被关闭)时显示重试入口,并通过 onError 上报。\r\n *\r\n * @example\r\n * ```tsx\r\n * // 弹窗模式:不中断用户当前操作\r\n * <RequireAuth autoLogin usePopup>\r\n * <Dashboard />\r\n * </RequireAuth>\r\n * ```\r\n */\r\nexport function RequireAuth({\r\n children,\r\n fallback,\r\n autoLogin = true,\r\n usePopup = false,\r\n onError,\r\n renderLoginError,\r\n}: RequireAuthProps) {\r\n const { isAuthenticated, isLoading, login, loginPopup } = useSso();\r\n const loginTriggeredRef = useRef(false);\r\n const [loginError, setLoginError] = useState<unknown>(null);\r\n\r\n const triggerLogin = () => {\r\n loginTriggeredRef.current = true;\r\n const currentPath = window.location.pathname + window.location.search;\r\n\r\n const handleFailure = (err: unknown) => {\r\n // 失败时重置触发标记,允许重试;否则组件会永久停在\"请先登录\"\r\n loginTriggeredRef.current = false;\r\n setLoginError(err);\r\n onError?.(err);\r\n };\r\n\r\n if (usePopup) {\r\n loginPopup({ returnUrl: currentPath }).catch((err) => {\r\n // 弹窗被拦截时回退到同页重定向\r\n if ((err as { code?: string }).code === \"popup_blocked\") {\r\n login(currentPath).catch(handleFailure);\r\n return;\r\n }\r\n // popup_closed 等其他错误:展示重试入口\r\n handleFailure(err);\r\n });\r\n } else {\r\n login(currentPath).catch(handleFailure);\r\n }\r\n };\r\n\r\n const retry = () => {\r\n setLoginError(null);\r\n triggerLogin();\r\n };\r\n\r\n useEffect(() => {\r\n if (!isLoading && !isAuthenticated && autoLogin && !loginTriggeredRef.current) {\r\n triggerLogin();\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [isLoading, isAuthenticated, autoLogin, usePopup, login, loginPopup]);\r\n\r\n if (isLoading) {\r\n return (\r\n fallback || React.createElement(\"div\", null, \"正在验证登录状态...\")\r\n );\r\n }\r\n\r\n if (!isAuthenticated) {\r\n if (loginError) {\r\n if (renderLoginError) {\r\n return React.createElement(\r\n React.Fragment,\r\n null,\r\n renderLoginError(loginError, retry)\r\n );\r\n }\r\n const message =\r\n loginError instanceof Error ? loginError.message : \"登录失败\";\r\n return React.createElement(\r\n \"div\",\r\n null,\r\n React.createElement(\"p\", null, `登录未完成:${message}`),\r\n React.createElement(\"button\", { onClick: retry }, \"重试登录\")\r\n );\r\n }\r\n if (fallback) return React.createElement(React.Fragment, null, fallback);\r\n return React.createElement(\"div\", null, \"请先登录\");\r\n }\r\n\r\n return React.createElement(React.Fragment, null, children);\r\n}\r\n\r\n// ============================================\r\n// withAuth HOC\r\n// ============================================\r\n\r\n/**\r\n * 高阶组件:包装页面组件,要求认证后才能访问\r\n *\r\n * @example\r\n * ```tsx\r\n * function DashboardPage() { return <div>Dashboard</div>; }\r\n * export default withAuth(DashboardPage);\r\n * ```\r\n */\r\nexport function withAuth<P extends object>(\r\n Component: ComponentType<P>\r\n): ComponentType<P> {\r\n const displayName =\r\n Component.displayName || Component.name || \"Component\";\r\n\r\n function WrappedComponent(props: P) {\r\n return React.createElement(\r\n RequireAuth,\r\n null,\r\n React.createElement(Component, props)\r\n );\r\n }\r\n\r\n WrappedComponent.displayName = `withAuth(${displayName})`;\r\n return WrappedComponent;\r\n}\r\n","/**\n * CallbackPage — 通用 OAuth 回调页面组件\n *\n * 子项目只需在回调路由渲染此组件即可完成 code → token 交换。\n * 交换成功后默认整页跳转到 returnUrl 或首页;\n * 传入 onSuccess 可跳过默认跳转,由 SPA 路由接管(保持应用状态)。\n *\n * @example\n * ```tsx\n * // 在回调页面路由中:\n * import { CallbackPage } from \"@nihplod/sso-sdk/react\";\n * export default function AuthCallback() {\n * return <CallbackPage />;\n * }\n * ```\n *\n * @example SPA 路由接管跳转(不整页刷新):\n * ```tsx\n * <CallbackPage onSuccess={() => navigate(\"/dashboard\", { replace: true })} />\n * ```\n */\n\n\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { useSso } from \"./SsoProvider\";\nimport { getReturnUrl, removeReturnUrl, type TokenData } from \"../core/storage\";\nimport { isTrustedReturnUrl } from \"../core/security\";\nimport { SsoError } from \"../core/errors\";\n\nexport interface CallbackPageProps {\n /**\n * 登录成功回调。传入后跳过默认的整页跳转(window.location.href),\n * 由调用方用 SPA 路由接管跳转,避免丢失应用内状态。\n */\n onSuccess?: (tokenData: TokenData) => void;\n\n /** 登录失败回调(错误同时会展示在错误页,除非提供了 renderError) */\n onError?: (error: Error) => void;\n\n /**\n * 自定义错误页渲染。不传时使用默认错误 UI(DefaultCallbackError)。\n */\n renderError?: (error: string) => React.ReactNode;\n}\n\n/** 默认错误页 UI(可通过 renderError 完全替换) */\nexport function DefaultCallbackError({ error }: { error: string }) {\n return React.createElement(\n \"div\",\n {\n style: {\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: \"100vh\",\n fontFamily: \"system-ui, sans-serif\",\n },\n },\n React.createElement(\n \"p\",\n { style: { color: \"#dc2626\", marginBottom: \"1rem\" } },\n error\n ),\n React.createElement(\n \"a\",\n { href: \"/\", style: { color: \"#2563eb\", textDecoration: \"underline\" } },\n \"返回首页\"\n )\n );\n}\n\nexport function CallbackPage({ onSuccess, onError, renderError }: CallbackPageProps = {}) {\n const { client, refreshUser } = useSso();\n const [error, setError] = useState<string | null>(null);\n const [processing, setProcessing] = useState(true);\n // 弹窗模式:postMessage 已发送但超时未收到主窗口 ACK(主窗口监听未挂载等),\n // 提示用户手动关闭窗口,避免弹窗悬挂\n const [awaitingManualClose, setAwaitingManualClose] = useState(false);\n\n // useLatest 惯例:用 ref 保存最新的 onSuccess/onError,\n // 避免调用方传内联回调时父组件重渲染导致下方 effect 重跑、\n // 已消费的 code 被重复交换 token(闪现假错误页)。\n const onSuccessRef = useRef(onSuccess);\n const onErrorRef = useRef(onError);\n useEffect(() => {\n onSuccessRef.current = onSuccess;\n onErrorRef.current = onError;\n });\n\n useEffect(() => {\n // 弹窗模式:通过 postMessage 将回调 URL 传回主窗口,不自行处理\n if (window.opener && !window.opener.closed) {\n const nonce = new URL(window.location.href).searchParams.get(\"popup_nonce\");\n // opener 与弹窗同源时可直接读 origin;跨源访问会抛 SecurityError,回退到当前 origin\n let targetOrigin = window.location.origin;\n try {\n targetOrigin = window.opener.location.origin;\n } catch {\n // 跨源 opener:使用当前 origin(postMessage 会校验,不会泄露给第三方)\n }\n\n // postMessage 只发一次可能赶上主窗口监听未挂载而丢失;\n // 每 500ms 重发直到收到主窗口 ACK,超时 10s 提示手动关闭窗口\n const message = {\n type: \"nihplod_sso_popup_callback\",\n callbackUrl: window.location.href,\n nonce: nonce || undefined,\n };\n let acked = false;\n const send = () => {\n if (acked || !window.opener || window.opener.closed) return;\n window.opener.postMessage(message, targetOrigin);\n };\n send();\n const resendTimer = setInterval(send, 500);\n const ackTimeout = setTimeout(() => {\n if (acked) return;\n clearInterval(resendTimer);\n setAwaitingManualClose(true);\n }, 10_000);\n\n const handleAck = (event: MessageEvent) => {\n // origin 与 nonce 校验与主窗口侧对称:nonce 缺失时不校验(兼容旧主窗口),存在时必须匹配\n if (event.origin !== targetOrigin) return;\n if (!event.data || event.data.type !== \"nihplod_sso_popup_ack\") return;\n if (nonce && event.data.nonce !== nonce) return;\n acked = true;\n clearInterval(resendTimer);\n clearTimeout(ackTimeout);\n };\n window.addEventListener(\"message\", handleAck);\n\n // 微任务延迟,避免 effect 内同步 setState\n Promise.resolve().then(() => setProcessing(false));\n\n return () => {\n clearInterval(resendTimer);\n clearTimeout(ackTimeout);\n window.removeEventListener(\"message\", handleAck);\n };\n }\n\n let cancelled = false;\n\n async function handleCallback() {\n try {\n const tokenData = await client.handleCallback(window.location.href);\n\n if (cancelled) return;\n\n // 静默探测(prompt=none)无 SSO 会话:handleCallback 返回 null。\n // 跳回 returnUrl 并附 sso_probe=no_session,子站据此按\"未登录\"展示\n if (tokenData === null) {\n const clientId = client.config.clientId;\n const probeReturnUrl = getReturnUrl(clientId);\n removeReturnUrl(clientId);\n const target =\n probeReturnUrl && isTrustedReturnUrl(probeReturnUrl, window.location.origin)\n ? probeReturnUrl\n : \"/\";\n window.location.href =\n target + (target.includes(\"?\") ? \"&\" : \"?\") + \"sso_probe=no_session\";\n return;\n }\n\n // 刷新用户信息\n await refreshUser();\n\n if (cancelled) return;\n\n // 读取并清除 returnUrl(按 clientId 隔离,与 login() 写入的 key 对应)\n const clientId = client.config.clientId;\n const returnUrl = getReturnUrl(clientId);\n removeReturnUrl(clientId);\n\n // 传入 onSuccess 时由调用方接管跳转(SPA 路由),跳过默认整页跳转\n const onSuccessCb = onSuccessRef.current;\n if (onSuccessCb) {\n onSuccessCb(tokenData);\n return;\n }\n\n // 默认:整页跳转到 returnUrl 或首页(开放重定向防护:仅相对路径或同源)\n // token 默认存 sessionStorage,整页跳转后登录态仍保留\n window.location.href =\n returnUrl && isTrustedReturnUrl(returnUrl, window.location.origin)\n ? returnUrl\n : \"/\";\n } catch (err) {\n if (cancelled) return;\n const errorObj = err instanceof Error ? err : new Error(String(err));\n onErrorRef.current?.(errorObj);\n if (errorObj instanceof SsoError) {\n // SsoError 携带 code + description,可直接展示\n setError(errorObj.description || `SSO 错误 (${errorObj.code})`);\n } else {\n setError(`登录回调处理失败: ${errorObj.message}`);\n }\n setProcessing(false);\n }\n }\n\n handleCallback();\n\n return () => { cancelled = true; };\n }, [client, refreshUser]);\n\n if (error) {\n if (renderError) return React.createElement(React.Fragment, null, renderError(error));\n return React.createElement(DefaultCallbackError, { error });\n }\n\n if (awaitingManualClose) {\n return React.createElement(\n \"div\",\n {\n style: {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: \"100vh\",\n fontFamily: \"system-ui, sans-serif\",\n },\n },\n React.createElement(\"p\", null, \"登录已完成,请手动关闭此窗口\")\n );\n }\n\n if (processing) {\n return React.createElement(\n \"div\",\n {\n style: {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: \"100vh\",\n fontFamily: \"system-ui, sans-serif\",\n },\n },\n React.createElement(\"p\", null, \"正在处理登录回调...\")\n );\n }\n\n return null;\n}\n"],"mappings":"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","names":["React","import_react","React","import_react","React","clientId"]}
1
+ {"version":3,"sources":["../../src/react/index.ts","../../src/react/SsoProvider.tsx","../../src/core/errors.ts","../../src/core/pkce.ts","../../src/core/storage.ts","../../src/core/security.ts","../../src/core/id-token.ts","../../src/core/http.ts","../../src/core/SsoClient.ts","../../src/react/RequireAuth.tsx","../../src/react/CallbackPage.tsx"],"sourcesContent":["/**\r\n * @nihplod/sso-sdk/react\r\n *\r\n * React 绑定层:SsoProvider Context、useSso Hook、RequireAuth 路由保护、CallbackPage 回调处理。\r\n */\r\n\r\nexport { SsoProvider, useSso } from \"./SsoProvider\";\r\nexport { RequireAuth, withAuth } from \"./RequireAuth\";\r\nexport { CallbackPage, DefaultCallbackError } from \"./CallbackPage\";\r\nexport type { CallbackPageProps } from \"./CallbackPage\";\r\n","/**\r\n * SsoProvider - React Context Provider\r\n *\r\n * 包裹子项目根组件,提供全局 SSO 认证状态管理:\r\n * - 自动管理 token 刷新定时器(过期前 60s 静默刷新)\r\n * - 监听 storage 事件实现跨 Tab 同步\r\n * - 提供 useSso() hook\r\n */\r\n\r\n\"use client\";\r\n\r\nimport React, {\r\n createContext,\r\n useContext,\r\n useState,\r\n useEffect,\r\n useCallback,\r\n useRef,\r\n type ReactNode,\r\n} from \"react\";\r\nimport { SsoClient } from \"../core/SsoClient\";\r\nimport { getTokenData, removeTokenData, saveTokenData } from \"../core/storage\";\r\nimport type { TokenData } from \"../core/storage\";\r\nimport { SsoError } from \"../core/errors\";\r\nimport type { SsoClientConfig, SsoUser } from \"../core/SsoClient\";\r\n\r\n// ============================================\r\n// Context 类型\r\n// ============================================\r\n\r\n/**\r\n * 认证状态为三态:\r\n * - isLoading=true:初始化/刷新中,user 与 error 均可能为 null\r\n * - error 非 null:加载用户信息失败(如会话已失效),user 为 null\r\n * - isAuthenticated=true 且 user 非 null:已登录\r\n */\r\nexport interface SsoContextValue {\r\n /** 当前用户信息 */\r\n user: SsoUser | null;\r\n\r\n /** 是否已认证 */\r\n isAuthenticated: boolean;\r\n\r\n /** 是否正在加载(初始化/刷新中) */\r\n isLoading: boolean;\r\n\r\n /** 最近一次加载用户信息失败的错误(成功或登出后为 null) */\r\n error: SsoError | null;\r\n\r\n /** 发起登录(同页重定向) */\r\n login: (returnUrl?: string) => Promise<void>;\r\n\r\n /** 弹窗模式登录(保持当前页面状态不丢失) */\r\n loginPopup: (options?: { returnUrl?: string; width?: number; height?: number }) => Promise<TokenData>;\r\n\r\n /** 登出 */\r\n logout: (redirectToSso?: boolean) => Promise<void>;\r\n\r\n /** 刷新用户信息 */\r\n refreshUser: () => Promise<void>;\r\n\r\n /** 获取 access_token(自动刷新过期 token) */\r\n getAccessToken: () => Promise<string | null>;\r\n\r\n /**\r\n * 会话已失效(refresh_token 被撤销/过期),需要用户重新登录。\r\n * 与 `error` 不同:该标志在 loadUser 拿到\"无 token\"时不会被清空,\r\n * 登录成功后才复位,便于子站稳定展示\"登录已过期\"提示。\r\n */\r\n sessionExpired: boolean;\r\n\r\n /** SsoClient 实例(高级用法) */\r\n client: SsoClient;\r\n}\r\n\r\nconst SsoContext = createContext<SsoContextValue | null>(null);\r\n\r\n// ============================================\r\n// Provider Props\r\n// ============================================\r\n\r\nexport interface SsoProviderProps {\r\n /** SSO 客户端配置 */\r\n config: SsoClientConfig;\r\n\r\n /** 子组件 */\r\n children: ReactNode;\r\n\r\n /**\r\n * 自动刷新阈值(秒)\r\n * access_token 过期前多少秒触发静默刷新。\r\n * 默认 60 秒。\r\n */\r\n refreshThreshold?: number;\r\n\r\n /**\r\n * Token 静默刷新成功后的回调(可选)。\r\n * 每次刷新成功时以新的 access_token 调用,可用于同步 token 到外部状态;\r\n * SDK 不会据此自动重试先前失败的 API 请求,重试需由调用方自行实现。\r\n */\r\n onTokenRefreshed?: (token: string) => void;\r\n\r\n /**\r\n * 会话失效回调(可选)。refresh_token 被撤销/过期时触发一次,\r\n * 子站可据此展示\"登录已过期,请重新登录\"或埋点上报。\r\n */\r\n onSessionExpired?: (error: SsoError) => void;\r\n}\r\n\r\n// 跨 Tab 刷新锁(基于 localStorage + 时间戳,避免多 Tab 同时刷新导致旧 RT 被撤销)\r\nconst REFRESH_LOCK_PREFIX = \"nihplod_sso_refresh_lock:\";\r\nconst LOCK_TTL_MS = 5000;\r\n\r\n// 跨 Tab 登录态事件(BroadcastChannel):默认 token 存 sessionStorage,\r\n// storage 事件不会触发,需显式广播让其他 Tab 感知登出/token 轮换\r\nconst CHANNEL_PREFIX = \"nihplod_sso_events:\";\r\n\r\n/** 当前 Tab 标识:用于忽略自己发出的广播(同 Tab 的监听对象也会收到消息) */\r\nconst TAB_ID = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\r\n\r\n/**\r\n * 广播跨 Tab 登录态事件(logout / token)。\r\n * BroadcastChannel 不可用(旧浏览器/隐私模式)时静默跳过,行为退化为原有方式。\r\n *\r\n * token 事件可携带刷新后的完整 TokenData,接收方直接落盘,\r\n * 避免其他 Tab 因 sessionStorage 不共享而重复刷新(旧 RT 已被轮换撤销)。\r\n * BroadcastChannel 是同源信道,与 sessionStorage 处于同一信任域:\r\n * 能读取广播数据的脚本同样能读本 Tab 的存储,不引入新的暴露面。\r\n */\r\nexport function broadcastSsoEvent(\r\n clientId: string,\r\n type: \"logout\" | \"token\",\r\n tokenData?: TokenData\r\n): void {\r\n if (typeof BroadcastChannel === \"undefined\") return;\r\n try {\r\n const channel = new BroadcastChannel(CHANNEL_PREFIX + clientId);\r\n // logout 不携带 tokenData(无意义且扩大暴露面)\r\n channel.postMessage({\r\n type,\r\n sourceTabId: TAB_ID,\r\n ...(type === \"token\" && tokenData ? { tokenData } : {}),\r\n });\r\n channel.close();\r\n } catch {\r\n // 忽略:广播失败不影响主流程\r\n }\r\n}\r\n\r\n// 本 Tab 持有的锁 token(用于释放时校验所有权,避免误删其他 Tab 的锁)\r\nconst ownedLocks = new Map<string, string>();\r\n\r\nfunction lockKey(clientId: string): string {\r\n return REFRESH_LOCK_PREFIX + clientId;\r\n}\r\n\r\n/**\r\n * 尝试获取跨 Tab 刷新锁。\r\n *\r\n * 权衡说明:localStorage 没有原子的 check-and-set,多 Tab 并发时无法完全避免\r\n * 竞态(两个 Tab 同毫秒通过存在性检查后都写入)。这里采用「写入唯一 token 后\r\n * 立刻 read-back 校验」将竞态窗口缩到最小;残余的双刷新风险由 SSO 服务端\r\n * refresh_token 轮换宽限期兜底,属于可接受权衡。\r\n */\r\nfunction acquireRefreshLock(clientId: string): boolean {\r\n if (typeof localStorage === \"undefined\") return true;\r\n const key = lockKey(clientId);\r\n const now = Date.now();\r\n const raw = localStorage.getItem(key);\r\n if (raw) {\r\n const ts = parseInt(raw, 10);\r\n if (!isNaN(ts) && now - ts < LOCK_TTL_MS) {\r\n return false;\r\n }\r\n }\r\n const token = `${now}:${Math.random().toString(36).slice(2)}`;\r\n localStorage.setItem(key, token);\r\n // read-back:若读回的不是自己写入的值,说明其他 Tab 并发抢锁成功\r\n if (localStorage.getItem(key) !== token) return false;\r\n ownedLocks.set(clientId, token);\r\n return true;\r\n}\r\n\r\nfunction releaseRefreshLock(clientId: string): void {\r\n if (typeof localStorage === \"undefined\") return;\r\n const owned = ownedLocks.get(clientId);\r\n ownedLocks.delete(clientId);\r\n // 仅当锁仍归本 Tab 所有时才删除,避免误删其他 Tab 新获取的锁\r\n if (owned && localStorage.getItem(lockKey(clientId)) === owned) {\r\n localStorage.removeItem(lockKey(clientId));\r\n }\r\n}\r\n\r\n/**\r\n * 在跨 Tab 互斥锁下执行静默刷新。\r\n *\r\n * 优先使用 Web Locks API(navigator.locks)实现浏览器级真互斥:\r\n * 拿到锁的 Tab 执行刷新;其他 Tab 以 ifAvailable 模式拿到 null lock\r\n * 直接返回,等待 storage 事件同步刷新结果。navigator.locks 不存在\r\n * 或调用异常(如隐私模式禁用)时回退 localStorage 锁逻辑。\r\n *\r\n * @returns true 表示本 Tab 持锁执行了刷新;false 表示未抢到锁(其他 Tab 正在刷新)\r\n *\r\n * 注意:task 必须自行消化内部异常(不得向外抛出),以便\r\n * navigator.locks.request 的 reject 可安全视为锁 API 故障并回退。\r\n */\r\nexport async function withRefreshLock(clientId: string, task: () => Promise<void>): Promise<boolean> {\r\n if (\r\n typeof navigator !== \"undefined\" &&\r\n typeof navigator.locks?.request === \"function\"\r\n ) {\r\n try {\r\n let ran = false;\r\n await navigator.locks.request(\r\n `nihplod_sso_refresh_${clientId}`,\r\n { ifAvailable: true },\r\n async (lock) => {\r\n if (!lock) return;\r\n ran = true;\r\n await task();\r\n }\r\n );\r\n return ran;\r\n } catch {\r\n // Web Locks API 故障,回退 localStorage 锁\r\n }\r\n }\r\n\r\n if (!acquireRefreshLock(clientId)) return false;\r\n try {\r\n await task();\r\n return true;\r\n } finally {\r\n releaseRefreshLock(clientId);\r\n }\r\n}\r\n\r\n// ============================================\r\n// Provider 实现\r\n// ============================================\r\n\r\nexport function SsoProvider({\r\n config,\r\n children,\r\n refreshThreshold = 60,\r\n onTokenRefreshed,\r\n onSessionExpired,\r\n}: SsoProviderProps) {\r\n const [user, setUser] = useState<SsoUser | null>(null);\r\n const [isLoading, setIsLoading] = useState(true);\r\n const [isAuthenticated, setIsAuthenticated] = useState(false);\r\n const [error, setError] = useState<SsoError | null>(null);\r\n const [sessionExpired, setSessionExpired] = useState(false);\r\n // 实例用 useState 懒初始化保持稳定引用(避免渲染期读取 ref)\r\n const [client] = useState(() => new SsoClient(config));\r\n const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n const loadedRef = useRef(false);\r\n // 回调 ref:调用方传内联函数时不触发 loadUser/refresh 依赖变化\r\n const onSessionExpiredRef = useRef(onSessionExpired);\r\n useEffect(() => {\r\n onSessionExpiredRef.current = onSessionExpired;\r\n });\r\n // 每次\"会话失效\"仅通知一次(登录成功/显式登出后复位),避免重复回调\r\n const sessionExpiredNotifiedRef = useRef(false);\r\n // 连续可恢复刷新失败计数(网络抖动/网关异常):达上限才判定会话终结\r\n const refreshFailureCountRef = useRef(0);\r\n const MAX_REFRESH_FAILURES = 3;\r\n\r\n /** 标记会话失效:保留 error/sessionExpired 供调用方展示,并(仅一次)触发回调 */\r\n const markSessionExpired = useCallback((error: SsoError) => {\r\n setError(error);\r\n setSessionExpired(true);\r\n if (!sessionExpiredNotifiedRef.current) {\r\n sessionExpiredNotifiedRef.current = true;\r\n onSessionExpiredRef.current?.(error);\r\n }\r\n }, []);\r\n\r\n // 加载用户信息\r\n const loadUser = useCallback(async () => {\r\n // 以「本地存在 token 数据」为准而非 isAuthenticated():\r\n // access_token 已过期时 getTokenData 仍返回数据(保留 refresh_token),\r\n // getUserInfo 内部会自动刷新恢复会话,避免过期即丢登录态。\r\n const tokenData = getTokenData(client.config.clientId);\r\n if (!tokenData) {\r\n setUser(null);\r\n setIsAuthenticated(false);\r\n // 不在此处清空 error/sessionExpired:会话过期提示需保留给调用方,\r\n // 显式登出路径会主动复位(见 logout)\r\n setIsLoading(false);\r\n return;\r\n }\r\n\r\n try {\r\n const u = await client.getUserInfo();\r\n setUser(u);\r\n setIsAuthenticated(true);\r\n setError(null);\r\n setSessionExpired(false);\r\n sessionExpiredNotifiedRef.current = false;\r\n refreshFailureCountRef.current = 0;\r\n } catch (err) {\r\n setUser(null);\r\n setIsAuthenticated(false);\r\n const errorObj =\r\n err instanceof SsoError\r\n ? err\r\n : new SsoError(\"userinfo_failed\", err instanceof Error ? err.message : String(err));\r\n // 写入 error 状态,调用方可据此展示\"会话失效,请重新登录\"等提示\r\n setError(errorObj);\r\n // 仅鉴权类错误(会话失效/未认证)清除本地 token;网络瞬断等可恢复错误\r\n // 保留 token,避免一次抖动就强制重新登录,下次加载/刷新会自动重试\r\n if (errorObj.code === \"not_authenticated\" || errorObj.code === \"session_expired\") {\r\n removeTokenData(client.config.clientId);\r\n markSessionExpired(errorObj);\r\n }\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n }, [client, markSessionExpired]);\r\n\r\n // 初始化(微任务延迟,避免 effect 内同步 setState)\r\n useEffect(() => {\r\n if (loadedRef.current) return;\r\n loadedRef.current = true;\r\n\r\n Promise.resolve().then(() => {\r\n // 本地存在 token 数据即尝试加载(过期 token 会在 loadUser 内自动刷新)\r\n if (getTokenData(client.config.clientId)) {\r\n loadUser();\r\n } else {\r\n setIsLoading(false);\r\n }\r\n });\r\n }, [client, loadUser]);\r\n\r\n // Token 自动刷新定时器(setTimeout 递归调度,避免 setInterval 堆积)\r\n useEffect(() => {\r\n let active = true;\r\n\r\n // 跨 Tab 互斥下的静默刷新:拿到锁的 Tab 执行刷新,\r\n // 刷新失败可能是其他 Tab 已刷新,短暂后重新加载本地 token\r\n const attemptRefresh = () =>\r\n withRefreshLock(client.config.clientId, async () => {\r\n try {\r\n const td = await client.refreshToken();\r\n refreshFailureCountRef.current = 0;\r\n onTokenRefreshed?.(td.access_token);\r\n // 携带新 token 广播:sessionStorage 不跨 Tab,其他 Tab 凭它直接落盘,\r\n // 否则它们本地旧 RT 已被轮换撤销,自行刷新必然 invalid_grant\r\n broadcastSsoEvent(client.config.clientId, \"token\", td);\r\n loadUser();\r\n } catch (err) {\r\n // refresh_token 被撤销/过期:立即保留\"会话已过期\"信号(否则 loadUser 走\r\n // 无 token 分支会静默掉线,调用方拿不到任何提示)\r\n if (\r\n err instanceof SsoError &&\r\n (err.code === \"session_expired\" ||\r\n err.code === \"no_refresh_token\" ||\r\n err.code === \"not_authenticated\")\r\n ) {\r\n removeTokenData(client.config.clientId);\r\n markSessionExpired(err);\r\n } else {\r\n // 网络抖动/网关异常等可恢复失败:计数,连续失败达上限才判定会话终结\r\n // (避免一次断网就强制登出丢状态)\r\n refreshFailureCountRef.current += 1;\r\n if (refreshFailureCountRef.current >= MAX_REFRESH_FAILURES) {\r\n markSessionExpired(\r\n err instanceof SsoError\r\n ? err\r\n : new SsoError(\"session_expired\", \"多次刷新失败,请重新登录\")\r\n );\r\n }\r\n }\r\n setTimeout(() => loadUser(), 500);\r\n }\r\n }).finally(() => {\r\n // 无论是否抢到锁/刷新成功都重新排程:\r\n // - 成功:按新 token 的过期时间排程\r\n // - 失败/未抢到锁:minDelayMs(5s)后重试,避免紧密循环;\r\n // token 也可能已被其他 Tab 刷新,重新排程会自然按新过期时间走\r\n if (active) scheduleNextRefresh(5_000);\r\n });\r\n\r\n const scheduleNextRefresh = (minDelayMs = 0) => {\r\n if (!active) return;\r\n\r\n // 清除已有定时器\r\n if (refreshTimerRef.current) {\r\n clearTimeout(refreshTimerRef.current);\r\n refreshTimerRef.current = null;\r\n }\r\n\r\n const tokenData = getTokenData(client.config.clientId);\r\n if (!tokenData) return;\r\n\r\n // 计算距离过期还有多少秒\r\n const remainingSec = (tokenData.expires_at - Date.now()) / 1000;\r\n\r\n // 已过期或即将过期:短延迟后带锁刷新(统一经定时器,保证失败后仍会重新排程)\r\n if (remainingSec <= 0 || remainingSec <= refreshThreshold) {\r\n refreshTimerRef.current = setTimeout(\r\n () => {\r\n if (active) void attemptRefresh();\r\n },\r\n Math.max(minDelayMs, 1000)\r\n );\r\n return;\r\n }\r\n\r\n // 在过期前 refreshThreshold 秒触发刷新\r\n const delayMs = Math.max((remainingSec - refreshThreshold) * 1000, minDelayMs, 1000);\r\n\r\n refreshTimerRef.current = setTimeout(() => {\r\n if (!active) return;\r\n const td = getTokenData(client.config.clientId);\r\n if (!td) return;\r\n\r\n const secLeft = (td.expires_at - Date.now()) / 1000;\r\n if (secLeft <= refreshThreshold) {\r\n void attemptRefresh();\r\n } else {\r\n // token 已被其他 Tab 刷新(storage 事件同步),按新过期时间重新排程\r\n scheduleNextRefresh();\r\n }\r\n }, delayMs);\r\n };\r\n\r\n scheduleNextRefresh();\r\n\r\n return () => {\r\n active = false;\r\n if (refreshTimerRef.current) {\r\n clearTimeout(refreshTimerRef.current);\r\n refreshTimerRef.current = null;\r\n }\r\n };\r\n }, [client, loadUser, refreshThreshold, onTokenRefreshed]);\r\n\r\n // 监听 storage 事件实现跨 Tab 同步(仅 persist 存储生效)\r\n useEffect(() => {\r\n const handleStorageChange = (e: StorageEvent) => {\r\n if (\r\n e.key &&\r\n e.key.startsWith(\"nihplod_sso_\") &&\r\n !e.key.startsWith(REFRESH_LOCK_PREFIX)\r\n ) {\r\n loadUser();\r\n }\r\n };\r\n\r\n window.addEventListener(\"storage\", handleStorageChange);\r\n return () => window.removeEventListener(\"storage\", handleStorageChange);\r\n }, [loadUser]);\r\n\r\n // 跨 Tab 事件同步(BroadcastChannel):补足 sessionStorage 默认存储下 storage 事件不触发的缺口。\r\n // 忽略自己发出的消息(同 Tab 监听对象也会收到):token 事件由发起方自行 loadUser,\r\n // logout 事件按\"全局登出\"处理——清除本地 token 并同步 UI,避免其他 Tab 定时刷新把用户\"登回来\"。\r\n // BroadcastChannel 是同源信道,与 sessionStorage 同一信任域:能读广播的脚本同样能读本 Tab 存储。\r\n // token 消息携带完整 TokenData 时直接落盘(轻量形状校验,防畸形数据写坏存储),\r\n // 本 Tab 的刷新定时器无需取消:到点时重读 getTokenData,secLeft > refreshThreshold 会自然重新排程。\r\n useEffect(() => {\r\n if (typeof BroadcastChannel === \"undefined\") return;\r\n const channel = new BroadcastChannel(CHANNEL_PREFIX + client.config.clientId);\r\n channel.onmessage = (event: MessageEvent) => {\r\n const data = event.data as {\r\n type?: string;\r\n sourceTabId?: string;\r\n tokenData?: unknown;\r\n } | null;\r\n if (!data || data.sourceTabId === TAB_ID) return;\r\n if (data.type === \"logout\") {\r\n removeTokenData(client.config.clientId);\r\n setUser(null);\r\n setIsAuthenticated(false);\r\n setError(null);\r\n setSessionExpired(false);\r\n sessionExpiredNotifiedRef.current = false;\r\n refreshFailureCountRef.current = 0;\r\n } else if (data.type === \"token\") {\r\n const td = data.tokenData as Partial<TokenData> | null | undefined;\r\n if (\r\n td &&\r\n typeof td.access_token === \"string\" &&\r\n typeof td.expires_at === \"number\" &&\r\n typeof td.refresh_token === \"string\"\r\n ) {\r\n // 携带完整 TokenData:直接落盘,本 Tab 无需再刷新(旧 RT 已被轮换撤销)\r\n saveTokenData(td as TokenData, client.config.clientId);\r\n }\r\n // 无 tokenData(旧版 SDK 的 Tab)时退化为原有行为:loadUser 内部自行处理\r\n loadUser();\r\n }\r\n };\r\n return () => channel.close();\r\n }, [client, loadUser]);\r\n\r\n // 登录\r\n const login = useCallback(\r\n async (returnUrl?: string) => {\r\n await client.login(returnUrl);\r\n },\r\n [client]\r\n );\r\n\r\n // 弹窗模式登录\r\n const loginPopup = useCallback(\r\n async (options?: { returnUrl?: string; width?: number; height?: number }) => {\r\n const tokenData = await client.loginPopup(options);\r\n await loadUser();\r\n return tokenData;\r\n },\r\n [client, loadUser]\r\n );\r\n\r\n // 登出(本地优先:先同步 UI 状态,再等待服务端撤销/跳转,避免按钮\"没反应\")\r\n const logout = useCallback(\r\n async (redirectToSso: boolean = false) => {\r\n setUser(null);\r\n setIsAuthenticated(false);\r\n setError(null);\r\n setSessionExpired(false);\r\n sessionExpiredNotifiedRef.current = false;\r\n refreshFailureCountRef.current = 0;\r\n // 通知其他 Tab 同步登出(sessionStorage 默认存储下 storage 事件不触发)\r\n broadcastSsoEvent(client.config.clientId, \"logout\");\r\n try {\r\n await client.logout(redirectToSso);\r\n } catch {\r\n // 本地已登出;撤销/跳转失败不阻塞调用方\r\n }\r\n },\r\n [client]\r\n );\r\n\r\n // 刷新用户信息\r\n const refreshUser = useCallback(async () => {\r\n await loadUser();\r\n }, [loadUser]);\r\n\r\n // 获取 access_token\r\n const getAccessToken = useCallback(async (): Promise<string | null> => {\r\n return client.getAccessToken();\r\n }, [client]);\r\n\r\n const value: SsoContextValue = {\r\n user,\r\n isAuthenticated,\r\n isLoading,\r\n error,\r\n login,\r\n loginPopup,\r\n logout,\r\n refreshUser,\r\n getAccessToken,\r\n sessionExpired,\r\n client,\r\n };\r\n\r\n return React.createElement(SsoContext.Provider, { value }, children);\r\n}\r\n\r\n// ============================================\r\n// Hook\r\n// ============================================\r\n\r\n/**\r\n * useSso Hook\r\n *\r\n * 在 SsoProvider 内部使用,获取 SSO 认证状态和操作方法。\r\n *\r\n * @example\r\n * ```tsx\r\n * function MyComponent() {\r\n * const { user, isAuthenticated, login, logout } = useSso();\r\n * if (!isAuthenticated) return <button onClick={() => login()}>登录</button>;\r\n * return <div>欢迎, {user?.nickname}</div>;\r\n * }\r\n * ```\r\n */\r\nexport function useSso(): SsoContextValue {\r\n const ctx = useContext(SsoContext);\r\n if (!ctx) {\r\n throw new Error(\"useSso() 必须在 <SsoProvider> 内部使用\");\r\n }\r\n return ctx;\r\n}\r\n","/**\r\n * SSO SDK 错误类型\r\n */\r\n\r\n/** SSO 错误码 */\r\nexport type SsoErrorCode =\r\n | \"invalid_config\"\r\n | \"state_mismatch\"\r\n | \"pkce_required\"\r\n | \"token_request_failed\"\r\n | \"session_expired\"\r\n | \"no_refresh_token\"\r\n | \"userinfo_failed\"\r\n | \"not_authenticated\"\r\n | \"authorization_code_expired\"\r\n // 保留:当前 SDK 不主动抛出,预留给服务端错误透传/未来版本使用\r\n | \"authorization_code_used\"\r\n | \"client_disabled\"\r\n | \"user_denied_authorization\"\r\n // 保留:当前 SDK 不主动抛出,预留给服务端错误透传/未来版本使用\r\n | \"account_disabled\"\r\n | \"sso_server_error\"\r\n | \"rate_limited\"\r\n | \"network_error\"\r\n | \"popup_blocked\"\r\n | \"popup_closed\"\r\n | \"id_token_invalid\"\r\n | \"id_token_unsupported_alg\"\r\n | \"id_token_hs256_unsupported\"\r\n // 保留:当前 SDK 不主动抛出(HS256 一律拒绝,见 id_token_hs256_unsupported)\r\n | \"id_token_missing_secret\"\r\n | \"id_token_invalid_signature\"\r\n | \"id_token_issuer_mismatch\"\r\n | \"id_token_audience_mismatch\"\r\n | \"id_token_expired\"\r\n | \"id_token_missing_sub\"\r\n | \"id_token_at_hash_mismatch\"\r\n | \"id_token_nonce_mismatch\"\r\n | \"logout_token_invalid\"\r\n | \"logout_token_unsupported_alg\"\r\n | \"logout_token_invalid_signature\"\r\n | \"logout_token_issuer_mismatch\"\r\n | \"logout_token_audience_mismatch\"\r\n | \"logout_token_expired\"\r\n | \"logout_token_replay\";\r\n\r\n/**\r\n * 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode\r\n *\r\n * @param oauthError 服务端返回的 error 字段\r\n * @param context 调用上下文:\"token_exchange\"(授权码换 token)或 \"refresh\"(刷新)。\r\n * invalid_grant 在换 token 场景多为授权码过期/已用,在刷新场景多为会话失效,\r\n * 需按上下文细化映射。\r\n */\r\nexport function mapOAuthErrorToSsoCode(\r\n oauthError: string,\r\n context: \"token_exchange\" | \"refresh\" = \"token_exchange\"\r\n): SsoErrorCode {\r\n switch (oauthError) {\r\n case \"invalid_grant\":\r\n // 刷新场景的 invalid_grant 表示 refresh_token 已撤销/过期(调用方另会清除本地登录态)\r\n return context === \"refresh\" ? \"session_expired\" : \"authorization_code_expired\";\r\n case \"invalid_client\":\r\n return \"client_disabled\";\r\n case \"access_denied\":\r\n return \"user_denied_authorization\";\r\n case \"server_error\":\r\n return \"sso_server_error\";\r\n case \"rate_limited\":\r\n // 限流是独立的可重试错误,不应误报为 network_error\r\n return \"rate_limited\";\r\n case \"invalid_scope\":\r\n case \"invalid_request\":\r\n case \"unauthorized_client\":\r\n case \"unsupported_grant_type\":\r\n default:\r\n return \"token_request_failed\";\r\n }\r\n}\r\n\r\n/**\r\n * SSO SDK 自定义错误\r\n */\r\nexport class SsoError extends Error {\r\n public readonly code: SsoErrorCode;\r\n public readonly description: string;\r\n public readonly cause?: unknown;\r\n\r\n constructor(code: SsoErrorCode, description: string, cause?: unknown) {\r\n super(`[SSO SDK] ${code}: ${description}`);\r\n this.name = \"SsoError\";\r\n this.code = code;\r\n this.description = description;\r\n this.cause = cause;\r\n }\r\n}\r\n\r\n/**\r\n * OAuth 2.0 协议层错误(RFC 6749 §5.2)\r\n *\r\n * 保留用于表示服务端原样返回的 OAuth 错误(error / error_description / error_uri),\r\n * 与 SDK 语义的 SsoError 区分。当前 SDK 内部不抛出此类型(统一使用 SsoError +\r\n * mapOAuthErrorToSsoCode 映射),作为公共类型导出供子项目自行解析 OAuth 响应时使用。\r\n */\r\nexport class OAuthError extends Error {\r\n public readonly code: string;\r\n public readonly description: string;\r\n public readonly uri?: string;\r\n\r\n constructor(code: string, description: string, uri?: string) {\r\n super(`[OAuth] ${code}: ${description}`);\r\n this.name = \"OAuthError\";\r\n this.code = code;\r\n this.description = description;\r\n this.uri = uri;\r\n }\r\n}\r\n","/**\r\n * PKCE (Proof Key for Code Exchange) 工具函数\r\n *\r\n * 实现 RFC 7636 规范:\r\n * - code_verifier: 43-128 字符的随机字符串\r\n * - code_challenge: SHA-256(code_verifier) 的 base64url 编码\r\n *\r\n * 仅支持 S256 模式,不提供 plain 模式。\r\n */\r\n\r\nconst VALID_CHARS =\r\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~\";\r\n\r\n/**\r\n * 生成密码学安全随机字符串\r\n *\r\n * 使用拒绝采样法(rejection sampling)避免取模非均匀分布。\r\n * VALID_CHARS 有 66 个字符,Uint8(0-255) 取模会偏向前 58 个字符。\r\n * 丢弃 >=198 的字节值使分布完全均匀(198 = floor(256/66) * 66)。\r\n */\r\nfunction generateRandomString(length: number): string {\r\n const mask = 198; // 66 * 3,最大均匀取值\r\n let result = \"\";\r\n\r\n while (result.length < length) {\r\n // 一次生成分块避免过小 array 的循环开销\r\n const chunkSize = Math.min(length * 2, 256);\r\n const array = new Uint8Array(chunkSize);\r\n crypto.getRandomValues(array);\r\n\r\n for (let i = 0; i < chunkSize && result.length < length; i++) {\r\n if (array[i] >= mask) continue; // 拒绝采样:丢弃 >=198 的字节\r\n result += VALID_CHARS[array[i] % VALID_CHARS.length];\r\n }\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * 生成 code_verifier\r\n *\r\n * 生成 43-128 字符之间的安全随机字符串,\r\n * 使用 `crypto.getRandomValues` 保证密码学安全性。\r\n *\r\n * @param length - 长度(默认 64),范围 43-128\r\n */\r\nexport function generateCodeVerifier(length: number = 64): string {\r\n if (length < 43 || length > 128) {\r\n throw new Error(\"code_verifier length must be between 43 and 128\");\r\n }\r\n return generateRandomString(length);\r\n}\r\n\r\n/**\r\n * 计算 code_challenge (S256)\r\n *\r\n * 对 code_verifier 进行 SHA-256 哈希,然后进行 base64url 编码。\r\n *\r\n * @param codeVerifier - PKCE code_verifier\r\n */\r\nexport async function generateCodeChallenge(\r\n codeVerifier: string\r\n): Promise<string> {\r\n const encoder = new TextEncoder();\r\n const data = encoder.encode(codeVerifier);\r\n const hash = await crypto.subtle.digest(\"SHA-256\", data);\r\n\r\n // 转为 base64url(无 padding)\r\n const bytes = new Uint8Array(hash);\r\n let binary = \"\";\r\n for (let i = 0; i < bytes.byteLength; i++) {\r\n binary += String.fromCharCode(bytes[i]);\r\n }\r\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\r\n}\r\n\r\n/**\r\n * 生成 state 参数\r\n *\r\n * 32 字节 hex 随机字符串,用于 CSRF 防护。\r\n */\r\nexport function generateState(): string {\r\n const array = new Uint8Array(32);\r\n crypto.getRandomValues(array);\r\n return Array.from(array, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\r\n}\r\n","/**\n * Token 存储抽象层\n *\n * 分两类存储:\n * - Token 数据:默认使用 sessionStorage(标签页级持久化)。登录回调后 CallbackPage\n * 默认整页跳转,内存存储会丢失登录态,因此默认改为 sessionStorage:\n * 整页跳转与刷新后登录态保留,关闭标签页自动清除;SSR / 隐私模式写入失败时\n * 降级为内存 Map。对需要多 Tab 共享 token 或 BFF/Confidential Client 场景,\n * 可通过 setTokenStorage() 注入 localStorage 实现(如 createSecureStorage({ persist: true }))。\n * - 临时数据(PKCE verifier / state / OIDC nonce / returnUrl / popup nonce):必须跨整页重定向存活\n * (login() 会 302 跳转到 SSO 中心再回来),因此默认写入 sessionStorage;\n * SSR 等无 sessionStorage 环境自动降级为内存 Map。\n *\n * 多 client 隔离:\n * - token / state / return_url 均支持按 clientId 隔离 key\n * - 不传 clientId 时使用全局 key,保持向后兼容\n */\n\n/** Token 数据 */\nexport interface TokenData {\n access_token: string;\n token_type: string;\n expires_in: number;\n refresh_token: string;\n id_token?: string;\n\n /** Token 签发时间(epoch ms),用于计算过期 */\n issued_at: number;\n\n /** 访问令牌实际过期时间(epoch ms)= issued_at + expires_in * 1000 */\n expires_at: number;\n}\n\n/** 存储接口 */\nexport interface TokenStorage {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n /**\n * 可选:枚举当前存储中的 key(不含 SDK 内部前缀)。\n * 供 clearAllSsoData() 清理 client 级残留(`token:<clientId>` 等)。\n * 自定义存储若不实现,clearAllSsoData() 仅能按已知 key 清理。\n */\n keys?(): string[];\n}\n\n/** 默认 localStorage/sessionStorage key 前缀 */\nconst STORAGE_PREFIX = \"nihplod_sso_\";\n\n/** 常量 key 名称 */\nconst TOKEN_KEY = \"token\";\nconst VERIFIER_KEY_PREFIX = \"pkce_verifier_\";\nconst STATE_KEY = \"oauth_state\";\nconst NONCE_KEY = \"oidc_nonce\";\nconst RETURN_URL_KEY = \"return_url\";\nconst LOGOUT_STATE_KEY = \"logout_state\";\n// 静默登录探测标记(prompt=none 发起的 authorize 请求所携带的 state)\nconst SILENT_PROBE_KEY = \"silent_probe\";\n\nfunction buildKey(base: string, clientId?: string): string {\n return clientId ? `${base}:${clientId}` : base;\n}\n\n/**\n * 内存存储适配器\n *\n * 仅作为 sessionStorage 不可用(SSR / 隐私模式写入失败)时的降级方案,\n * 或显式注入使用。页面刷新后用户需重新登录。\n *\n * 代价:多 Tab 间不会自动同步 token;SDK 内部已用锁机制避免并发刷新。\n */\nfunction createMemoryStorageAdapter(): TokenStorage {\n const store = new Map<string, string>();\n return {\n get(key: string) {\n return store.get(key) ?? null;\n },\n set(key: string, value: string) {\n store.set(key, value);\n },\n remove(key: string) {\n store.delete(key);\n },\n keys() {\n return [...store.keys()];\n },\n };\n}\n\nconst memoryStorageAdapter = createMemoryStorageAdapter();\n\n/**\n * 浏览器 localStorage 实现\n *\n * 适用于:\n * - Next.js BFF / Confidential Client(refresh_token 不直接暴露给浏览器)。\n * - 需要多 Tab 共享 token 的场景。\n *\n * 注意:Public Client 在浏览器中直接存储 refresh_token 会增加 XSS 风险,\n * 建议改用内存存储或 Service Worker 封装。\n */\nconst localStorageAdapter: TokenStorage = {\n get(key: string) {\n try {\n if (typeof localStorage === \"undefined\") return null;\n return localStorage.getItem(STORAGE_PREFIX + key);\n } catch {\n // 沙箱 iframe / 禁用存储等场景访问 localStorage 会抛 SecurityError\n return null;\n }\n },\n set(key: string, value: string) {\n try {\n if (typeof localStorage === \"undefined\") return;\n localStorage.setItem(STORAGE_PREFIX + key, value);\n } catch (err) {\n // QuotaExceededError / SecurityError 等写入失败不应中断登录流程\n console.warn(\n `[SSO SDK] localStorage 写入失败(${err instanceof Error ? err.name : String(err)}),数据未持久化`\n );\n }\n },\n remove(key: string) {\n try {\n if (typeof localStorage === \"undefined\") return;\n localStorage.removeItem(STORAGE_PREFIX + key);\n } catch {\n // 忽略存储不可用\n }\n },\n keys() {\n try {\n if (typeof localStorage === \"undefined\") return [];\n const result: string[] = [];\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n if (key?.startsWith(STORAGE_PREFIX)) result.push(key.slice(STORAGE_PREFIX.length));\n }\n return result;\n } catch {\n return [];\n }\n },\n};\n\n/**\n * 浏览器 sessionStorage 实现\n *\n * 同时承担两个角色:\n * - Token 数据的默认存储:整页重定向(CallbackPage 默认行为)与页面刷新后\n * 登录态仍保留;sessionStorage 随标签页关闭自动清除,且不跨 Tab 共享,\n * 比 localStorage 缩小了 XSS 窃取 refresh_token 的暴露面。\n * - 临时数据(PKCE verifier / state / returnUrl / popup nonce)专用:\n * 必须跨整页重定向存活,不能放模块级内存。\n * SSR / 隐私模式写入失败时降级为内存 Map,保证不抛异常。\n */\nfunction createSessionStorageAdapter(): TokenStorage {\n const fallback = new Map<string, string>();\n return {\n get(key: string) {\n try {\n if (typeof sessionStorage !== \"undefined\") {\n return sessionStorage.getItem(STORAGE_PREFIX + key) ?? fallback.get(key) ?? null;\n }\n } catch {\n // 沙箱 iframe / 禁用存储:退化为内存 fallback\n }\n return fallback.get(key) ?? null;\n },\n set(key: string, value: string) {\n try {\n if (typeof sessionStorage !== \"undefined\") {\n sessionStorage.setItem(STORAGE_PREFIX + key, value);\n return;\n }\n } catch {\n // 隐私模式等场景写入失败时降级到内存\n }\n fallback.set(key, value);\n },\n remove(key: string) {\n fallback.delete(key);\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.removeItem(STORAGE_PREFIX + key);\n } catch {\n // 忽略存储不可用\n }\n },\n keys() {\n const result = new Set<string>(fallback.keys());\n try {\n if (typeof sessionStorage !== \"undefined\") {\n for (let i = 0; i < sessionStorage.length; i++) {\n const key = sessionStorage.key(i);\n if (key?.startsWith(STORAGE_PREFIX)) result.add(key.slice(STORAGE_PREFIX.length));\n }\n }\n } catch {\n // 存储不可用:仅返回 fallback\n }\n return [...result];\n },\n };\n}\n\n/** 临时数据存储(跨整页重定向存活),独立于 token 存储,不受 setTokenStorage() 影响 */\nconst _transient: TokenStorage = createSessionStorageAdapter();\n\n/**\n * 创建存储实现\n *\n * @param options.persist 是否持久化到 localStorage。默认 false(sessionStorage)。\n * - false(默认):sessionStorage,标签页级持久化(刷新/整页跳转后登录态保留,\n * 关闭标签页即清除),多 Tab 间不共享;隐私模式等写入失败时降级为内存。\n * - true:localStorage,多 Tab 共享、关闭浏览器后仍保留。\n * ⚠️ 安全警告:persist=true 会将 refresh_token 明文写入 localStorage,\n * 任何 XSS 均可在不被检测的情况下读取。仅在 BFF/Confidential Client\n * 且 refresh_token 不直接暴露给浏览器的场景下使用。\n */\nexport function createSecureStorage(options: { persist?: boolean } = {}): TokenStorage {\n return options.persist ? localStorageAdapter : createSessionStorageAdapter();\n}\n\n/**\n * 默认 token 存储:sessionStorage(标签页级持久化),SSR/隐私模式降级为内存。\n * 可通过 setTokenStorage() 覆盖。\n */\nlet _storage: TokenStorage = createSessionStorageAdapter();\n\n/**\n * 设置自定义存储实现\n */\nexport function setTokenStorage(storage: TokenStorage): void {\n _storage = storage;\n}\n\n/**\n * 获取当前存储实现\n */\nexport function getTokenStorage(): TokenStorage {\n return _storage;\n}\n\n// ============================================\n// Token 存取\n// ============================================\n\nexport function saveTokenData(data: TokenData, clientId?: string): void {\n _storage.set(buildKey(TOKEN_KEY, clientId), JSON.stringify(data));\n}\n\nexport function getTokenData(clientId?: string): TokenData | null {\n const raw = _storage.get(buildKey(TOKEN_KEY, clientId));\n if (!raw) return null;\n try {\n // 过期不物理删除:refresh_token 通常比 access_token 长寿,\n // 删除会同时丢弃 refresh_token 使刷新路径失效。\n // 是否过期由调用方依据 expires_at 判断(isAuthenticated / getUserInfo 等)。\n return JSON.parse(raw) as TokenData;\n } catch {\n return null;\n }\n}\n\nexport function removeTokenData(clientId?: string): void {\n _storage.remove(buildKey(TOKEN_KEY, clientId));\n}\n\n// ============================================\n// PKCE 临时数据存取(transient:sessionStorage,跨整页重定向存活)\n// ============================================\n\nexport function savePkceVerifier(clientId: string, verifier: string): void {\n _transient.set(VERIFIER_KEY_PREFIX + clientId, verifier);\n}\n\nexport function getPkceVerifier(clientId: string): string | null {\n return _transient.get(VERIFIER_KEY_PREFIX + clientId);\n}\n\nexport function removePkceVerifier(clientId: string): void {\n _transient.remove(VERIFIER_KEY_PREFIX + clientId);\n}\n\n// ============================================\n// State 参数存取(transient)\n// ============================================\n\nexport function saveOAuthState(state: string, clientId?: string): void {\n _transient.set(buildKey(STATE_KEY, clientId), state);\n}\n\nexport function getOAuthState(clientId?: string): string | null {\n return _transient.get(buildKey(STATE_KEY, clientId));\n}\n\nexport function removeOAuthState(clientId?: string): void {\n _transient.remove(buildKey(STATE_KEY, clientId));\n}\n\n// ============================================\n// OIDC nonce 存取(transient,ID Token 重放防护)\n// ============================================\n\nexport function saveOAuthNonce(nonce: string, clientId?: string): void {\n _transient.set(buildKey(NONCE_KEY, clientId), nonce);\n}\n\nexport function getOAuthNonce(clientId?: string): string | null {\n return _transient.get(buildKey(NONCE_KEY, clientId));\n}\n\nexport function removeOAuthNonce(clientId?: string): void {\n _transient.remove(buildKey(NONCE_KEY, clientId));\n}\n\n// ============================================\n// Logout State 存取(transient,RP-Initiated Logout CSRF 防护)\n// ============================================\n\nexport function saveLogoutState(state: string, clientId?: string): void {\n _transient.set(buildKey(LOGOUT_STATE_KEY, clientId), state);\n}\n\nexport function getLogoutState(clientId?: string): string | null {\n return _transient.get(buildKey(LOGOUT_STATE_KEY, clientId));\n}\n\nexport function removeLogoutState(clientId?: string): void {\n _transient.remove(buildKey(LOGOUT_STATE_KEY, clientId));\n}\n\n// ============================================\n// 返回 URL 存取(transient)\n// ============================================\nexport function saveReturnUrl(url: string, clientId?: string): void {\n _transient.set(buildKey(RETURN_URL_KEY, clientId), url);\n}\n\nexport function getReturnUrl(clientId?: string): string | null {\n return _transient.get(buildKey(RETURN_URL_KEY, clientId));\n}\n\nexport function removeReturnUrl(clientId?: string): void {\n _transient.remove(buildKey(RETURN_URL_KEY, clientId));\n}\n\n// ============================================\n// 静默登录探测标记存取(transient,prompt=none)\n// ============================================\n// 存的是该次探测 authorize 请求的 state 值,回调时用于匹配\n// (只有 state 与标记一致才按\"探测无会话\"处理,防伪造回调)\n\nexport function saveSilentProbe(state: string, clientId?: string): void {\n _transient.set(buildKey(SILENT_PROBE_KEY, clientId), state);\n}\n\nexport function getSilentProbe(clientId?: string): string | null {\n return _transient.get(buildKey(SILENT_PROBE_KEY, clientId));\n}\n\nexport function removeSilentProbe(clientId?: string): void {\n _transient.remove(buildKey(SILENT_PROBE_KEY, clientId));\n}\n\n// ============================================\n// 清理所有 SSO 数据\n// ============================================\n\nexport function clearAllSsoData(clientId?: string): void {\n if (clientId) {\n removeTokenData(clientId);\n removeOAuthState(clientId);\n removeOAuthNonce(clientId);\n removeReturnUrl(clientId);\n removeLogoutState(clientId);\n removeSilentProbe(clientId);\n removePkceVerifier(clientId);\n // popup nonce 复用 verifier key 空间(`${clientId}_popup_nonce`)\n removePkceVerifier(`${clientId}_popup_nonce`);\n return;\n }\n\n removeTokenData();\n removeOAuthState();\n removeOAuthNonce();\n removeReturnUrl();\n removeLogoutState();\n removeSilentProbe();\n\n // 无参调用必须清理全部 client 级残留(token:<clientId>、oauth_state:<clientId>、\n // oidc_nonce:<clientId>、return_url:<clientId>、pkce_verifier_<clientId> 等)。\n // 枚举浏览器存储中所有带前缀的 key:从原生存储、临时数据存储与 token 存储同时移除,\n // 且访问存储属性本身也要 try/catch(沙箱 iframe 下会抛 SecurityError)。\n const stores: Storage[] = [];\n try {\n if (typeof sessionStorage !== \"undefined\") stores.push(sessionStorage);\n } catch {\n /* 存储不可用 */\n }\n try {\n if (typeof localStorage !== \"undefined\") stores.push(localStorage);\n } catch {\n /* 存储不可用 */\n }\n\n const abstractKeys = new Set<string>();\n for (const store of stores) {\n const keys: string[] = [];\n try {\n for (let i = 0; i < store.length; i++) {\n const key = store.key(i);\n if (key?.startsWith(STORAGE_PREFIX)) keys.push(key);\n }\n } catch {\n continue;\n }\n for (const key of keys) {\n abstractKeys.add(key.slice(STORAGE_PREFIX.length));\n try {\n store.removeItem(key);\n } catch {\n /* 忽略 */\n }\n }\n }\n\n // 适配器级枚举:覆盖自定义 TokenStorage / 内存 fallback 中的 client 级 key\n // (这些 key 不在浏览器存储中,仅靠上方枚举无法发现)\n for (const store of [_transient, _storage]) {\n try {\n for (const key of store.keys?.() ?? []) abstractKeys.add(key);\n } catch {\n /* 忽略自定义实现异常 */\n }\n }\n\n for (const key of abstractKeys) {\n _transient.remove(key);\n _storage.remove(key);\n }\n}\n\n/**\n * 清理指定 clientId 列表的 PKCE verifier\n *\n * 适用于非浏览器环境或已知 clientId 场景。\n */\nexport function clearVerifiersForClients(clientIds: string[]): void {\n for (const clientId of clientIds) {\n _transient.remove(VERIFIER_KEY_PREFIX + clientId);\n }\n}\n","/**\n * 安全相关小工具:returnUrl 开放重定向校验、常量时间字符串比较\n */\n\n/**\n * 反斜杠或 ASCII 控制字符(tab/newline/CR/DEL)。\n * WHATWG URL 解析会在解析前剥离 TAB/LF/CR,并把 \"\\\" 归一化为 \"/\",\n * 因此 \"/\\evil.com\"、\"/\\n//evil.com\" 能通过字符串前缀判断后变成跨站 URL。\n */\nconst UNSAFE_URL_CHAR_PATTERN = /[\\\\\\u0000-\\u001F\\u007F]/;\n\n/**\n * 校验 returnUrl 是否可信(防开放重定向)。\n * 仅允许:\n * - 站内相对路径(以单个 \"/\" 开头且不以 \"//\" 开头)\n * - 与 currentOrigin 完全同源的 http(s) 绝对 URL\n * 拒绝一切含反斜杠/控制字符的值(浏览器会将 \"/\\evil.com\" 解析为跨站 URL),\n * 拒绝 userinfo 与危险 scheme。\n */\nexport function isTrustedReturnUrl(url: string, currentOrigin: string): boolean {\n if (!url) return false;\n if (UNSAFE_URL_CHAR_PATTERN.test(url)) return false;\n if (url.startsWith(\"//\")) return false;\n if (url.startsWith(\"/\")) return true;\n try {\n const parsed = new URL(url);\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") return false;\n if (parsed.username || parsed.password) return false;\n return parsed.origin === currentOrigin;\n } catch {\n return false;\n }\n}\n\n/**\n * 常量时间字符串比较(避免 state / at_hash / nonce 等机密值的时序侧信道)。\n * 长度不同也执行完整循环,不提前返回。\n */\nexport function timingSafeEqualString(a: string, b: string): boolean {\n const ba = new TextEncoder().encode(a);\n const bb = new TextEncoder().encode(b);\n if (ba.length === 0 || bb.length === 0) return ba.length === bb.length;\n const len = Math.max(ba.length, bb.length);\n let diff = ba.length ^ bb.length;\n for (let i = 0; i < len; i++) {\n diff |= ba[i % ba.length] ^ bb[i % bb.length];\n }\n return diff === 0;\n}\n","/**\r\n * ID Token 验证工具(浏览器 / Edge Runtime 通用)\r\n *\r\n * 提取 SsoClient 与 Next.js callback 中重复的 ID Token 校验逻辑,\r\n * 包括 base64url 解码、JWKS 获取、RS256 签名验证、at_hash 计算等。\r\n */\r\n\r\nimport { SsoError } from \"./errors\";\r\nimport { timingSafeEqualString } from \"./security\";\r\n\r\n// 运行时 crypto.subtle 检测:现代浏览器 / Edge Runtime / Node 20+ 均提供全局 crypto\r\nconst _crypto: Pick<Crypto, \"subtle\"> | null =\r\n typeof crypto !== \"undefined\" && (crypto as Crypto).subtle ? (crypto as Crypto) : null;\r\n\r\nfunction getCrypto(): Crypto | null {\r\n return (_crypto as Crypto | null) ?? null;\r\n}\r\n\r\nexport interface JwksKey {\r\n kty: string;\r\n kid?: string;\r\n alg?: string;\r\n use?: string;\r\n n?: string;\r\n e?: string;\r\n x5c?: string[];\r\n}\r\n\r\nexport interface Jwks {\r\n keys: JwksKey[];\r\n}\r\n\r\nexport interface ValidateIdTokenResult {\r\n sub: string;\r\n}\r\n\r\nexport interface ValidateIdTokenOptions {\r\n /**\r\n * 发现 SSO 中心已启用 RS256 时,是否拒绝 HS256 ID Token。\r\n * 默认 true:只要 JWKS 中存在 RS256 签名密钥,就拒绝 HS256(安全推荐)。\r\n *\r\n * @deprecated 无实际作用:HS256 ID Token 一律被拒绝(对称密钥无法安全分发给\r\n * Public Client / BFF),该选项只影响走哪条抛错路径\r\n * (id_token_unsupported_alg 或 id_token_hs256_unsupported),\r\n * 不影响最终结果。保留仅为避免破坏 API,请勿依赖;将于未来大版本移除。\r\n */\r\n rejectHs256WhenRs256Available?: boolean;\r\n /**\r\n * 期望的 OIDC nonce(login 时生成并随 authorize 请求发送)。\r\n * 传入后 ID Token 必须携带 nonce claim 且与该值常量时间相等(fail-closed),\r\n * 否则抛出 id_token_nonce_mismatch,防止 ID Token 重放。\r\n * 不传时(如 refresh 流程):ID Token 若仍携带 nonce 会被拒绝(OIDC Core §12.2,\r\n * refresh 签发的 ID Token 不得携带 nonce),否则跳过 nonce 校验。\r\n */\r\n expectedNonce?: string;\r\n}\r\n\r\nfunction base64UrlDecode(input: string): Uint8Array {\r\n const base64 = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\r\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\r\n const binary = atob(padded);\r\n const bytes = new Uint8Array(binary.length);\r\n for (let i = 0; i < binary.length; i++) {\r\n bytes[i] = binary.charCodeAt(i);\r\n }\r\n return bytes;\r\n}\r\n\r\n/** 解码 JWT payload(不验证签名) */\r\nexport function decodeJwtPayload(token: string): Record<string, unknown> | null {\r\n try {\r\n const parts = token.split(\".\");\r\n if (parts.length !== 3) return null;\r\n const json = new TextDecoder().decode(base64UrlDecode(parts[1]));\r\n return JSON.parse(json) as Record<string, unknown>;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/** 解码 JWT header */\r\nexport function decodeJwtHeader(token: string): Record<string, unknown> | null {\r\n try {\r\n const parts = token.split(\".\");\r\n if (parts.length !== 3) return null;\r\n const json = new TextDecoder().decode(base64UrlDecode(parts[0]));\r\n return JSON.parse(json) as Record<string, unknown>;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/** 标准化 issuer/base URL,去除末尾斜杠 */\r\nfunction normalizeIssuer(url: string): string {\r\n return url.replace(/\\/+$/, \"\");\r\n}\r\n\r\n/** OIDC Discovery 文档(仅取本模块关心的字段) */\r\nexport interface OidcDiscoveryDoc {\r\n issuer?: string;\r\n jwks_uri?: string;\r\n}\r\n\r\nlet cachedJwks: { baseUrl: string; jwks: Jwks; fetchedAt: number } | null = null;\r\nlet cachedDiscovery: { baseUrl: string; doc: OidcDiscoveryDoc; fetchedAt: number } | null = null;\r\nconst JWKS_CACHE_TTL_MS = 5 * 60 * 1000;\r\n/** discovery / JWKS 拉取超时(与 SsoClient._getDiscovery 的 10s 一致) */\r\nconst FETCH_TIMEOUT_MS = 10000;\r\n\r\n/**\r\n * 拉取 OIDC Discovery 文档(带内存缓存;失败返回 null 不抛错)\r\n *\r\n * 只缓存成功结果:失败(网络错误 / 非 2xx)不缓存,主站短暂抖动不会让\r\n * 登录持续失败到 TTL 期满,下一次调用立即重试(与 core/discovery.ts 策略一致)。\r\n */\r\nexport async function fetchDiscoveryDoc(baseUrl: string): Promise<OidcDiscoveryDoc | null> {\r\n const now = Date.now();\r\n if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {\r\n return cachedDiscovery.doc;\r\n }\r\n\r\n try {\r\n const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {\r\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\r\n });\r\n if (!res.ok) return null;\r\n const doc = (await res.json()) as OidcDiscoveryDoc;\r\n cachedDiscovery = { baseUrl, doc, fetchedAt: now };\r\n return doc;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nexport interface FetchJwksOptions {\r\n /**\r\n * 绕过内存缓存与浏览器 HTTP 缓存(cache: \"no-cache\")强制重取 JWKS,\r\n * 并同步刷新模块级缓存。用于密钥轮换后 kid 失配的自愈重试。\r\n */\r\n forceRefresh?: boolean;\r\n}\r\n\r\n/** 从 SSO 中心拉取 JWKS(带内存缓存;优先 Discovery 的 jwks_uri,回退硬编码路径) */\r\nexport async function fetchJwks(baseUrl: string, options: FetchJwksOptions = {}): Promise<Jwks | null> {\r\n const now = Date.now();\r\n if (!options.forceRefresh && cachedJwks && cachedJwks.baseUrl === baseUrl && now - cachedJwks.fetchedAt < JWKS_CACHE_TTL_MS) {\r\n return cachedJwks.jwks;\r\n }\r\n\r\n const discovery = await fetchDiscoveryDoc(baseUrl);\r\n const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;\r\n\r\n try {\r\n const res = await fetch(jwksUri, {\r\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\r\n ...(options.forceRefresh ? { cache: \"no-cache\" as RequestCache } : {}),\r\n });\r\n if (!res.ok) return null;\r\n const jwks = (await res.json()) as Jwks;\r\n cachedJwks = { baseUrl, jwks, fetchedAt: now };\r\n return jwks;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/** 清空 JWKS / Discovery 内存缓存(测试与密钥轮换场景使用) */\r\nexport function clearIdTokenCaches(): void {\r\n cachedJwks = null;\r\n cachedDiscovery = null;\r\n}\r\n\r\n/** 使用 RS256 公钥验证 ID Token 签名 */\r\nexport async function verifyRs256Signature(token: string, jwk: JwksKey): Promise<boolean> {\r\n try {\r\n const [headerB64, payloadB64, signature] = token.split(\".\");\r\n if (!signature || !jwk.n || !jwk.e) return false;\r\n\r\n const c = getCrypto();\r\n if (!c) return false;\r\n\r\n const cryptoKey = await c.subtle.importKey(\r\n \"jwk\",\r\n { kty: \"RSA\", n: jwk.n, e: jwk.e, alg: \"RS256\", ext: false },\r\n { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" },\r\n false,\r\n [\"verify\"]\r\n );\r\n\r\n const signatureBytes = base64UrlDecode(signature);\r\n const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);\r\n return await c.subtle.verify(\r\n \"RSASSA-PKCS1-v1_5\",\r\n cryptoKey,\r\n signatureBytes as unknown as BufferSource,\r\n data as unknown as BufferSource\r\n );\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/** 计算 OIDC at_hash(access_token SHA-256 前 128bit base64url) */\r\nexport async function computeAtHash(accessToken: string): Promise<string> {\r\n const c = getCrypto();\r\n if (!c) return \"\";\r\n const hash = await c.subtle.digest(\"SHA-256\", new TextEncoder().encode(accessToken));\r\n const bytes = new Uint8Array(hash);\r\n const half = bytes.slice(0, bytes.length / 2);\r\n let binary = \"\";\r\n for (const b of half) {\r\n binary += String.fromCharCode(b);\r\n }\r\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\r\n}\r\n\r\n/** 验证 ID Token(iss、aud、exp、iat、azp、sub、签名、at_hash) */\r\nexport async function validateIdToken(\r\n idToken: string,\r\n accessToken: string,\r\n expectedIssuer: string,\r\n expectedClientId: string,\r\n options: ValidateIdTokenOptions = {}\r\n): Promise<ValidateIdTokenResult> {\r\n const { rejectHs256WhenRs256Available = true, expectedNonce } = options;\r\n\r\n const header = decodeJwtHeader(idToken);\r\n if (!header) {\r\n throw new SsoError(\"id_token_invalid\", \"ID Token 格式错误\");\r\n }\r\n\r\n const alg = header.alg;\r\n if (typeof alg !== \"string\" || (alg !== \"RS256\" && alg !== \"HS256\")) {\r\n throw new SsoError(\"id_token_unsupported_alg\", `不支持的 ID Token 签名算法: ${alg}`);\r\n }\r\n\r\n const baseUrl = normalizeIssuer(expectedIssuer);\r\n // issuer 以 Discovery 文档的 issuer 为准(防伪站伪造 iss 通过校验),回退调用方传入值\r\n const discovery = await fetchDiscoveryDoc(baseUrl);\r\n const normalizedIssuer = normalizeIssuer(discovery?.issuer || baseUrl);\r\n\r\n // RS256:通过 JWKS 验证签名\r\n if (alg === \"RS256\") {\r\n const jwks = await fetchJwks(baseUrl);\r\n if (!jwks) {\r\n throw new SsoError(\"id_token_invalid_signature\", \"无法获取 JWKS 验证 ID Token 签名\");\r\n }\r\n const kid = typeof header.kid === \"string\" ? header.kid : undefined;\r\n // 有 kid 时精确匹配;无 kid 时逐个尝试所有 RS256 签名公钥\r\n const matchCandidates = (set: Jwks) =>\r\n set.keys.filter(\r\n (k) =>\r\n k.kty === \"RSA\" &&\r\n k.alg === \"RS256\" &&\r\n k.use === \"sig\" &&\r\n (kid ? k.kid === kid : true)\r\n );\r\n const verifyAny = async (keys: JwksKey[]): Promise<boolean> => {\r\n for (const key of keys) {\r\n if (await verifyRs256Signature(idToken, key)) return true;\r\n }\r\n return false;\r\n };\r\n\r\n let candidates = matchCandidates(jwks);\r\n let validSig = candidates.length > 0 ? await verifyAny(candidates) : false;\r\n\r\n // 密钥轮换自愈:按 kid 过滤无候选,或所有候选验签失败时,\r\n // 可能是本地 JWKS 缓存陈旧 —— 强制重取一次 JWKS 后重试,仍失败才抛错\r\n if (candidates.length === 0 || !validSig) {\r\n const freshJwks = await fetchJwks(baseUrl, { forceRefresh: true });\r\n if (freshJwks) {\r\n candidates = matchCandidates(freshJwks);\r\n validSig = candidates.length > 0 ? await verifyAny(candidates) : false;\r\n }\r\n }\r\n\r\n if (candidates.length === 0) {\r\n throw new SsoError(\"id_token_invalid_signature\", \"JWKS 中未找到匹配的 RS256 公钥\");\r\n }\r\n if (!validSig) {\r\n throw new SsoError(\"id_token_invalid_signature\", \"ID Token 签名验证失败\");\r\n }\r\n }\r\n\r\n // HS256:对称密钥无法安全分发给 Public Client / BFF。\r\n // SDK 拒绝 HS256 ID Token:主站需配置 RS256 密钥对以启用安全验证。\r\n if (alg === \"HS256\") {\r\n if (rejectHs256WhenRs256Available) {\r\n const jwks = await fetchJwks(baseUrl);\r\n const hasRs256 = jwks?.keys?.some((k) => k.alg === \"RS256\" && k.use === \"sig\");\r\n if (hasRs256) {\r\n throw new SsoError(\"id_token_unsupported_alg\", \"SSO 中心已配置 RS256,拒绝 HS256 ID Token\");\r\n }\r\n }\r\n throw new SsoError(\r\n \"id_token_hs256_unsupported\",\r\n \"ID Token 使用 HS256 对称签名,SDK 无法安全验证。\" +\r\n \"请联系管理员在 SSO 主站配置 RS256 密钥对(JWT_ID_TOKEN_PRIVATE_KEY / JWT_ID_TOKEN_PUBLIC_KEY)后重新签发。\"\r\n );\r\n }\r\n\r\n const payload = decodeJwtPayload(idToken);\r\n if (!payload) {\r\n throw new SsoError(\"id_token_invalid\", \"ID Token payload 解析失败\");\r\n }\r\n\r\n const tokenIssuer = typeof payload.iss === \"string\" ? normalizeIssuer(payload.iss) : \"\";\r\n if (tokenIssuer !== normalizedIssuer) {\r\n throw new SsoError(\"id_token_issuer_mismatch\", \"ID Token issuer 不匹配\");\r\n }\r\n\r\n // aud 校验:单值或数组包含 clientId;多 aud 时按 OIDC Core §3.1.3.7 要求 azp 必须等于 clientId\r\n const aud = payload.aud;\r\n const audList = Array.isArray(aud) ? aud : typeof aud === \"string\" ? [aud] : [];\r\n if (!audList.includes(expectedClientId)) {\r\n throw new SsoError(\"id_token_audience_mismatch\", \"ID Token audience 不匹配\");\r\n }\r\n if (audList.length > 1 && payload.azp !== expectedClientId) {\r\n throw new SsoError(\"id_token_audience_mismatch\", \"ID Token 多 audience 时 azp 必须为当前 client\");\r\n }\r\n\r\n // exp 必需且不得过期(允许 60s clock skew,与 iat 校验对称)\r\n if (typeof payload.exp !== \"number\") {\r\n throw new SsoError(\"id_token_invalid\", \"ID Token 缺少 exp 声明\");\r\n }\r\n if (Date.now() >= payload.exp * 1000 + 60_000) {\r\n throw new SsoError(\"id_token_expired\", \"ID Token 已过期\");\r\n }\r\n\r\n // iat 必需(OIDC Core §2:ID Token 必须包含 iat)且不得在未来(允许 60s clock skew)\r\n if (typeof payload.iat !== \"number\") {\r\n throw new SsoError(\"id_token_invalid\", \"ID Token 缺少 iat 声明\");\r\n }\r\n if (payload.iat * 1000 > Date.now() + 60_000) {\r\n throw new SsoError(\"id_token_invalid\", \"ID Token iat 在未来,疑似伪造或时钟异常\");\r\n }\r\n\r\n if (typeof payload.sub !== \"string\" || !payload.sub) {\r\n throw new SsoError(\"id_token_missing_sub\", \"ID Token 缺少 sub\");\r\n }\r\n\r\n // 校验 at_hash:确保 ID Token 与当前 access_token 绑定(常量时间比较)\r\n if (typeof payload.at_hash === \"string\" && payload.at_hash) {\r\n const actual = await computeAtHash(accessToken);\r\n if (!timingSafeEqualString(actual, payload.at_hash)) {\r\n throw new SsoError(\"id_token_at_hash_mismatch\", \"ID Token at_hash 不匹配\");\r\n }\r\n }\r\n\r\n // 校验 nonce(OIDC Core §3.1.3.7):调用方传入 expectedNonce 时 fail-closed,\r\n // ID Token 必须携带 nonce 且与登录时生成的值常量时间相等,防 ID Token 重放。\r\n // 未传 expectedNonce(如本地 nonce cookie 已被清除/过期)但 ID Token 携带 nonce\r\n // 时同样拒绝(OIDC Core §12.2):该 token 声称绑定某次登录会话,本地却无法核对,\r\n // 可能是非本次登录签发的重放 token。refresh 签发的 ID Token 按规范不携带 nonce,\r\n // 不受影响(refresh 流程不传 expectedNonce 且 token 无 nonce claim)。\r\n if (expectedNonce !== undefined) {\r\n const tokenNonce = typeof payload.nonce === \"string\" ? payload.nonce : \"\";\r\n if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {\r\n throw new SsoError(\"id_token_nonce_mismatch\", \"ID Token nonce 不匹配\");\r\n }\r\n } else if (typeof payload.nonce === \"string\" && payload.nonce) {\r\n throw new SsoError(\r\n \"id_token_nonce_mismatch\",\r\n \"ID Token 携带 nonce 但本地无期望值(可能非本次登录会话签发)\"\r\n );\r\n }\r\n\r\n return { sub: payload.sub };\r\n}\r\n","/**\n * 网络请求工具:带超时的 fetch(AbortController)\n *\n * 内部模块:不从包入口导出(core/SsoClient 与 next/callback 共用)。\n */\n\n/** token / userinfo 请求默认超时(毫秒):避免回调页/登录流程无限挂起 */\nexport const REQUEST_TIMEOUT_MS = 10_000;\n\n/** 带超时的 fetch(AbortController);超时抛 AbortError(调用方按网络错误处理) */\nexport async function fetchWithTimeout(\n input: RequestInfo | URL,\n init: RequestInit = {},\n timeoutMs: number = REQUEST_TIMEOUT_MS\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(input, { ...init, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n }\n}\n","/**\r\n * SSO Client 核心类\r\n *\r\n * 封装完整的 OAuth 2.0 授权码 + PKCE S256 流程:\r\n * - login(): 发起授权请求\r\n * - handleCallback(): 处理回调,交换 token\r\n * - refreshToken(): 刷新 access token(原子轮换)\r\n * - getUserInfo(): 获取用户信息\r\n * - logout(): 登出\r\n * - isAuthenticated(): 检查认证状态\r\n */\r\n\r\nimport { SsoError, mapOAuthErrorToSsoCode } from \"./errors\";\r\nimport {\r\n generateCodeVerifier,\r\n generateCodeChallenge,\r\n generateState,\r\n} from \"./pkce\";\r\nimport {\r\n saveTokenData,\r\n getTokenData,\r\n removeTokenData,\r\n savePkceVerifier,\r\n getPkceVerifier,\r\n removePkceVerifier,\r\n saveOAuthState,\r\n getOAuthState,\r\n removeOAuthState,\r\n saveOAuthNonce,\r\n getOAuthNonce,\r\n removeOAuthNonce,\r\n saveLogoutState,\r\n getLogoutState,\r\n removeLogoutState,\r\n saveReturnUrl,\r\n saveSilentProbe,\r\n getSilentProbe,\r\n removeSilentProbe,\r\n clearAllSsoData,\r\n type TokenData,\r\n} from \"./storage\";\r\nimport { validateIdToken } from \"./id-token\";\r\nimport { isTrustedReturnUrl, timingSafeEqualString } from \"./security\";\r\nimport { fetchWithTimeout } from \"./http\";\r\n\r\n// ============================================\r\n// 网络请求超时\r\n// ============================================\r\n\r\n// fetchWithTimeout(默认超时 10s)已收敛到 ./http(与 next/callback 共用)\r\n/** revoke 为 best-effort,短超时避免登出等待过久 */\r\nconst REVOKE_TIMEOUT_MS = 3_000;\r\n/**\r\n * 过期判定提前量(毫秒):getAccessToken/getUserInfo 在 token 剩余有效期\r\n * 不足 10s 时即触发刷新,避免拿到的 token 在请求到达服务端时已过期\r\n */\r\nconst EXPIRY_SKEW_MS = 10_000;\r\n\r\n// ============================================\r\n// 类型定义\r\n// ============================================\r\n\r\n/**\r\n * Token 响应基本字段的防御性校验(fail-closed)。\r\n *\r\n * - access_token 必须为非空字符串;\r\n * - expires_in 必须为有限正数——否则 expires_at = NaN,本地会永久判定过期\r\n * (每次 getAccessToken/getUserInfo 都触发刷新),且刷新后仍写回 NaN;\r\n * - requireRefreshToken=true 时 refresh_token 必须为非空字符串:\r\n * 主站 authorization_code 交换必然签发 refresh_token(refresh 场景按\r\n * RFC 6749 §6 允许省略,调用方已做沿用旧值的回退),缺失即响应异常。\r\n */\r\nfunction assertValidTokenResponse(\r\n data: TokenResponse,\r\n requireRefreshToken: boolean\r\n): void {\r\n if (typeof data.access_token !== \"string\" || !data.access_token) {\r\n throw new SsoError(\"token_request_failed\", \"Token 响应缺少 access_token\");\r\n }\r\n if (\r\n typeof data.expires_in !== \"number\" ||\r\n !Number.isFinite(data.expires_in) ||\r\n data.expires_in <= 0\r\n ) {\r\n throw new SsoError(\r\n \"token_request_failed\",\r\n `Token 响应 expires_in 非法: ${String(data.expires_in)}`\r\n );\r\n }\r\n if (\r\n requireRefreshToken &&\r\n (typeof data.refresh_token !== \"string\" || !data.refresh_token)\r\n ) {\r\n throw new SsoError(\r\n \"token_request_failed\",\r\n \"Token 响应缺少 refresh_token(authorization_code 交换必须返回)\"\r\n );\r\n }\r\n}\r\n\r\n/** SSO 客户端配置 */\r\nexport interface SsoClientConfig {\r\n /** OAuth Client ID(从管理后台获取) */\r\n clientId: string;\r\n\r\n /** OAuth Client Secret(从管理后台获取)\r\n *\r\n * ⚠️ 安全提醒:client_secret 不应暴露在前端代码中。\r\n * 对于浏览器端 SPA(Public Client),应省略此字段;SDK 会使用 PKCE 完成授权,\r\n * 且 token 端点不会发送 client_secret。\r\n * 对于 BFF/Next.js 等 Confidential Client,可传入 client_secret。\r\n */\r\n clientSecret?: string;\r\n\r\n /** 回调 URL(必须与注册的 redirect_uri 完全一致) */\r\n redirectUri: string;\r\n\r\n /** SSO 中心地址,如 \"https://nihplod.cn\" */\r\n ssoBaseUrl: string;\r\n\r\n /**\r\n * 允许 Discovery 文档声明跨源端点(默认 false)。\r\n *\r\n * ⚠️ 安全警告:Discovery 文档若被篡改(如中间人/反向代理劫持),攻击者可把\r\n * token_endpoint 指向自己的服务器窃取 authorization code / refresh_token。\r\n * SDK 默认校验 authorization_endpoint 必须与 ssoBaseUrl 同源、\r\n * token_endpoint / userinfo_endpoint 必须与 ssoBaseUrl(或 serverBaseUrl)同源,\r\n * 不匹配时告警并回退到默认端点。仅当 SSO 中心确实把端点托管在其他域名时\r\n * 才设置此开关,并自行确保 Discovery 通道可信。\r\n */\r\n allowCrossOriginDiscoveryEndpoints?: boolean;\r\n\r\n /** 请求的 scope(空格分隔),如 \"openid profile phone\" */\r\n scopes?: string;\r\n\r\n /** RP-Initiated Logout 返回地址(可选)。不传时回退到 redirectUri */\r\n postLogoutRedirectUri?: string;\r\n\r\n /**\r\n * 调试日志开关(默认 false)。开启后输出登录/回调/刷新/登出等关键流程日志,\r\n * 便于接入方定位问题(生产默认关闭,避免日志噪音)。\r\n */\r\n debug?: boolean;\r\n\r\n /**\r\n * 服务端到服务端调用的内网地址(可选,如 http://127.0.0.1:3000)。\r\n * 仅用于 discovery / token / userinfo / revocation 等服务器间请求;\r\n * 浏览器跳转(authorize、end-session)始终使用 ssoBaseUrl 公网地址。\r\n * 仅在 BFF/服务端场景配置;浏览器端(Public Client)不要配置。\r\n */\r\n serverBaseUrl?: string;\r\n}\r\n\r\n/** 用户信息 */\r\nexport interface SsoUser {\r\n sub: string;\r\n nickname?: string;\r\n avatar?: string;\r\n /** 手机号(脱敏,兼容保留的非标准 claim,新代码请使用 phone_number) */\r\n phone?: string;\r\n /** 手机号(脱敏,OIDC 标准 claim,与 phone 内容一致) */\r\n phone_number?: string;\r\n /** 生日(ISO 8601,需 birthday scope,未设置时为 null) */\r\n birthday?: string | null;\r\n membership_level?: string;\r\n /** 累计消费金额(单位:元,需 membership scope,与 membership_level 同源下发) */\r\n total_spent?: number;\r\n total_points?: number;\r\n}\r\n\r\n/** login() / getLoginUrl() 选项 */\r\nexport interface LoginOptions {\r\n /**\r\n * OIDC prompt 参数:\r\n * - \"none\":静默探测 SSO 会话。用户在 IdP 有会话时直接回跳完成登录;\r\n * 无会话时 IdP 回跳 error=login_required(或 consent_required /\r\n * interaction_required),handleCallback() 识别后返回 null 而不抛错\r\n * (CallbackPage 默认跳回 returnUrl 并附 sso_probe=no_session)。\r\n * ⚠️ 不要在页面加载时自动探测(会打扰无会话用户并消耗请求),\r\n * 应由用户点击等明确动作触发。\r\n * - \"login\":强制重新认证;\r\n * - \"consent\":强制重新授权确认。\r\n */\r\n prompt?: \"none\" | \"login\" | \"consent\";\r\n}\r\n\r\n/** prompt=none 静默探测失败时 IdP 回跳的 error 值(OIDC Core §3.1.2.6) */\r\nconst SILENT_PROBE_ERRORS = new Set([\r\n \"login_required\",\r\n \"consent_required\",\r\n \"interaction_required\",\r\n]);\r\n\r\n/** Token 响应 */\r\nexport interface TokenResponse {\r\n access_token: string;\r\n token_type: string;\r\n expires_in: number;\r\n /**\r\n * authorization_code 交换主站必然返回(缺失时 handleCallback 抛\r\n * token_request_failed);refresh 响应按 RFC 6749 §6 可省略,\r\n * 此时 SDK 沿用旧 refresh_token。\r\n */\r\n refresh_token: string;\r\n id_token?: string;\r\n}\r\n\r\n/** OIDC Discovery 文档 */\r\nexport interface OidcDiscovery {\r\n issuer: string;\r\n authorization_endpoint: string;\r\n token_endpoint: string;\r\n userinfo_endpoint: string;\r\n jwks_uri: string;\r\n introspection_endpoint: string;\r\n revocation_endpoint?: string;\r\n end_session_endpoint?: string;\r\n scopes_supported: string[];\r\n response_types_supported: string[];\r\n grant_types_supported: string[];\r\n code_challenge_methods_supported: string[];\r\n}\r\n\r\n// ============================================\r\n// SsoClient\r\n// ============================================\r\n\r\nexport class SsoClient {\r\n public readonly config: SsoClientConfig;\r\n /** 服务器间调用的基准地址(未配置 serverBaseUrl 时等于 ssoBaseUrl) */\r\n private readonly _serverBase: string;\r\n private _discovery: OidcDiscovery | null = null;\r\n private _discoveryFetchedAt: number = 0;\r\n private _refreshLock: Promise<TokenData> | null = null;\r\n /** Discovery 端点 origin 校验失败只告警一次(拒绝的文档不缓存,避免每次调用刷屏) */\r\n private _discoveryOriginWarned = false;\r\n\r\n /** Discovery 文档缓存 TTL(5 分钟) */\r\n private static readonly DISCOVERY_TTL_MS = 5 * 60 * 1000;\r\n\r\n /** Discovery fetch 超时(10 秒) */\r\n private static readonly DISCOVERY_TIMEOUT_MS = 10000;\r\n\r\n constructor(config: SsoClientConfig) {\r\n if (!config.clientId) throw new SsoError(\"invalid_config\", \"clientId 不能为空\");\r\n if (!config.redirectUri) throw new SsoError(\"invalid_config\", \"redirectUri 不能为空\");\r\n if (!config.ssoBaseUrl) throw new SsoError(\"invalid_config\", \"ssoBaseUrl 不能为空\");\r\n\r\n // 规范化 ssoBaseUrl:移除末尾斜杠\r\n const base = config.ssoBaseUrl.replace(/\\/+$/, \"\");\r\n this.config = { ...config, ssoBaseUrl: base };\r\n this._serverBase = (config.serverBaseUrl ?? base).replace(/\\/+$/, \"\");\r\n }\r\n\r\n // ============================================\r\n // 调试日志\r\n // ============================================\r\n\r\n /**\r\n * 调试日志(config.debug=true 时输出,前缀 [SSO SDK]):\r\n * 覆盖登录发起 / 回调结果 / 刷新失败 / 登出等关键节点,便于接入方定位问题。\r\n * 不会输出 token/授权码等敏感值。\r\n */\r\n debugLog(...args: unknown[]): void {\r\n if (this.config.debug) {\r\n console.warn(\"[SSO SDK]\", ...args);\r\n }\r\n }\r\n\r\n // ============================================\r\n // 内部方法\r\n // ============================================\r\n\r\n /**\r\n * 校验 Discovery 文档端点 origin(防文档被篡改后 token/code 被导向攻击者服务器):\r\n * - authorization_endpoint 必须与 ssoBaseUrl 同源(浏览器跳转目标);\r\n * - token_endpoint / userinfo_endpoint 必须与 ssoBaseUrl 同源,\r\n * 或(配置了 serverBaseUrl 时)与内网地址同源。\r\n * 任一不匹配:告警并返回 null(上层回退到默认端点,fail-safe)。\r\n * 可通过 config.allowCrossOriginDiscoveryEndpoints 关闭(见配置项警告)。\r\n */\r\n private _sanitizeDiscovery(d: OidcDiscovery): OidcDiscovery | null {\r\n if (this.config.allowCrossOriginDiscoveryEndpoints) return d;\r\n const allowedOrigins = new Set<string>();\r\n try {\r\n allowedOrigins.add(new URL(this.config.ssoBaseUrl).origin);\r\n allowedOrigins.add(new URL(this._serverBase).origin);\r\n } catch {\r\n return null;\r\n }\r\n const sameOrigin = (endpoint: string): boolean => {\r\n try {\r\n return allowedOrigins.has(new URL(endpoint).origin);\r\n } catch {\r\n return false;\r\n }\r\n };\r\n // 浏览器跳转端点只允许公网 origin(内网地址用户浏览器不可达)\r\n const publicOrigin = new URL(this.config.ssoBaseUrl).origin;\r\n let authorizationOk = false;\r\n try {\r\n authorizationOk = new URL(d.authorization_endpoint).origin === publicOrigin;\r\n } catch {\r\n authorizationOk = false;\r\n }\r\n const bad: string[] = [];\r\n if (!authorizationOk) bad.push(\"authorization_endpoint\");\r\n if (!sameOrigin(d.token_endpoint)) bad.push(\"token_endpoint\");\r\n if (!sameOrigin(d.userinfo_endpoint)) bad.push(\"userinfo_endpoint\");\r\n if (bad.length > 0) {\r\n if (!this._discoveryOriginWarned) {\r\n this._discoveryOriginWarned = true;\r\n console.warn(\r\n `[SSO SDK] OIDC Discovery 端点 origin 与 ssoBaseUrl/serverBaseUrl 不匹配(${bad.join(\", \")}),` +\r\n \"已回退到默认端点。若 SSO 中心确实托管跨源端点,请设置 allowCrossOriginDiscoveryEndpoints: true\"\r\n );\r\n }\r\n return null;\r\n }\r\n return d;\r\n }\r\n\r\n /**\r\n * 获取 OIDC Discovery 文档(带缓存 + 超时)\r\n *\r\n * 缓存 5 分钟,超时 10 秒。\r\n * 失败时返回 null(上层调用方回退到硬编码默认端点)。\r\n */\r\n private async _getDiscovery(): Promise<OidcDiscovery | null> {\r\n const now = Date.now();\r\n\r\n // 命中缓存且未过期\r\n if (\r\n this._discovery &&\r\n now - this._discoveryFetchedAt < SsoClient.DISCOVERY_TTL_MS\r\n ) {\r\n return this._discovery;\r\n }\r\n\r\n // discovery 是服务器间调用:配置了 serverBaseUrl 时经内网拉取\r\n // (文档内容由 SSO 中心按公网 origin 生成,与拉取通道无关)\r\n const url = `${this._serverBase}/api/oauth/.well-known/openid-configuration`;\r\n\r\n try {\r\n const controller = new AbortController();\r\n const timeoutId = setTimeout(\r\n () => controller.abort(),\r\n SsoClient.DISCOVERY_TIMEOUT_MS\r\n );\r\n\r\n const res = await fetch(url, { signal: controller.signal });\r\n clearTimeout(timeoutId);\r\n\r\n if (!res.ok) {\r\n console.warn(\r\n `[SSO SDK] OIDC Discovery 请求失败: HTTP ${res.status}, 回退到默认端点`\r\n );\r\n return null;\r\n }\r\n\r\n const doc = (await res.json()) as OidcDiscovery;\r\n // 端点 origin 校验:不匹配的文档视为不可用,回退默认端点(不缓存)\r\n const sanitized = this._sanitizeDiscovery(doc);\r\n if (!sanitized) return null;\r\n this._discovery = sanitized;\r\n this._discoveryFetchedAt = now;\r\n return this._discovery;\r\n } catch (err) {\r\n console.warn(\r\n `[SSO SDK] OIDC Discovery 请求异常: ${err instanceof Error ? err.message : String(err)}, 回退到默认端点`\r\n );\r\n // 不清空已有缓存(即使过期也优于无数据)\r\n return this._discovery || null;\r\n }\r\n }\r\n\r\n /** 获取 authorize 端点 URL(优先 Discovery,回退默认) */\r\n private async _getAuthorizeEndpoint(): Promise<string> {\r\n const d = await this._getDiscovery();\r\n if (d) return d.authorization_endpoint;\r\n return `${this.config.ssoBaseUrl}/api/oauth/authorize`;\r\n }\r\n\r\n /** 获取 token 端点 URL(服务器间调用:serverBaseUrl 直连优先,其次 Discovery,回退默认) */\r\n private async _getTokenEndpoint(): Promise<string> {\r\n if (this.config.serverBaseUrl) return `${this._serverBase}/api/oauth/token`;\r\n const d = await this._getDiscovery();\r\n if (d) return d.token_endpoint;\r\n return `${this.config.ssoBaseUrl}/api/oauth/token`;\r\n }\r\n\r\n /** 获取 userinfo 端点 URL(服务器间调用:serverBaseUrl 直连优先,其次 Discovery,回退默认) */\r\n private async _getUserinfoEndpoint(): Promise<string> {\r\n if (this.config.serverBaseUrl) return `${this._serverBase}/api/oauth/userinfo`;\r\n const d = await this._getDiscovery();\r\n if (d) return d.userinfo_endpoint;\r\n return `${this.config.ssoBaseUrl}/api/oauth/userinfo`;\r\n }\r\n\r\n /**\r\n * 开放重定向防护:仅保存相对路径或与当前页面同源的 returnUrl,\r\n * 其余(如 https://evil.com)忽略并告警,防止回调后跳转到钓鱼站点。\r\n */\r\n private _saveReturnUrlIfTrusted(returnUrl: string): void {\r\n if (isTrustedReturnUrl(returnUrl, window.location.origin)) {\r\n // 按 clientId 隔离存储,与 CallbackPage 读取的 key 对应\r\n saveReturnUrl(returnUrl, this.config.clientId);\r\n } else {\r\n console.warn(`[SSO SDK] returnUrl 未通过同源校验,已忽略: ${returnUrl}`);\r\n }\r\n }\r\n\r\n // ============================================\r\n // 公共 API\r\n // ============================================\r\n\r\n /**\r\n * 发起 SSO 登录\r\n *\r\n * 生成 PKCE code_verifier/code_challenge 和 state 参数,\r\n * 构建 authorize URL,通过 302 跳转到 SSO 登录页。\r\n *\r\n * @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage;\r\n * 仅允许相对路径或同源绝对 URL,否则忽略并告警)\r\n * @param options.prompt - OIDC prompt 参数。传 \"none\" 为静默探测:\r\n * 额外在存储中记录探测标记(该次请求的 state),回调无会话时\r\n * handleCallback() 返回 null 而不抛错。\r\n *\r\n * ⚠️ 不要与 getLoginUrl() 混用:两者都会重新生成并覆盖 sessionStorage 中的\r\n * state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。\r\n * 同一次登录只使用其中一个入口。\r\n */\r\n async login(returnUrl?: string, options: LoginOptions = {}): Promise<void> {\r\n const verifier = generateCodeVerifier();\r\n const challenge = await generateCodeChallenge(verifier);\r\n const state = generateState();\r\n // OIDC nonce(256-bit 随机):绑定 ID Token 到本次登录会话,防重放\r\n const nonce = generateState();\r\n\r\n // 保存 PKCE verifier、state 和 nonce 到 sessionStorage(整页重定向后仍可读取)\r\n savePkceVerifier(this.config.clientId, verifier);\r\n saveOAuthState(state, this.config.clientId);\r\n saveOAuthNonce(nonce, this.config.clientId);\r\n // 静默探测标记:存该次请求的 state,回调凭它识别\"探测无会话\"\r\n if (options.prompt === \"none\") {\r\n saveSilentProbe(state, this.config.clientId);\r\n } else {\r\n removeSilentProbe(this.config.clientId);\r\n }\r\n\r\n if (returnUrl) {\r\n this._saveReturnUrlIfTrusted(returnUrl);\r\n }\r\n\r\n // 构建 authorize URL\r\n const authorizeEndpoint = await this._getAuthorizeEndpoint();\r\n const params = new URLSearchParams();\r\n params.set(\"response_type\", \"code\");\r\n params.set(\"client_id\", this.config.clientId);\r\n params.set(\"redirect_uri\", this.config.redirectUri);\r\n params.set(\"scope\", this.config.scopes || \"openid profile\");\r\n params.set(\"state\", state);\r\n params.set(\"nonce\", nonce);\r\n params.set(\"code_challenge\", challenge);\r\n params.set(\"code_challenge_method\", \"S256\");\r\n if (options.prompt) {\r\n params.set(\"prompt\", options.prompt);\r\n }\r\n\r\n // 跳转\r\n window.location.href = `${authorizeEndpoint}?${params.toString()}`;\r\n }\r\n\r\n /**\r\n * 构建登录 URL(不跳转,返回 URL 字符串)\r\n *\r\n * 适用于需要手动处理跳转的场景。\r\n *\r\n * ⚠️ 不要与 login() 混用:两者都会重新生成并覆盖 sessionStorage 中的\r\n * state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。\r\n * 同一次登录只使用其中一个入口。\r\n */\r\n async getLoginUrl(returnUrl?: string, options: LoginOptions = {}): Promise<string> {\r\n const verifier = generateCodeVerifier();\r\n const challenge = await generateCodeChallenge(verifier);\r\n const state = generateState();\r\n // OIDC nonce(与 state 同等强度的随机值),回调时校验 ID Token 的 nonce claim\r\n const nonce = generateState();\r\n\r\n savePkceVerifier(this.config.clientId, verifier);\r\n saveOAuthState(state, this.config.clientId);\r\n saveOAuthNonce(nonce, this.config.clientId);\r\n // 静默探测标记(与 login() 一致):prompt=none 时记录该次请求的 state\r\n if (options.prompt === \"none\") {\r\n saveSilentProbe(state, this.config.clientId);\r\n } else {\r\n removeSilentProbe(this.config.clientId);\r\n }\r\n // returnUrl 按 clientId 隔离,与 login() / CallbackPage 保持一致\r\n if (returnUrl) this._saveReturnUrlIfTrusted(returnUrl);\r\n\r\n const authorizeEndpoint = await this._getAuthorizeEndpoint();\r\n const params = new URLSearchParams();\r\n params.set(\"response_type\", \"code\");\r\n params.set(\"client_id\", this.config.clientId);\r\n params.set(\"redirect_uri\", this.config.redirectUri);\r\n params.set(\"scope\", this.config.scopes || \"openid profile\");\r\n params.set(\"state\", state);\r\n params.set(\"nonce\", nonce);\r\n params.set(\"code_challenge\", challenge);\r\n params.set(\"code_challenge_method\", \"S256\");\r\n if (options.prompt) {\r\n params.set(\"prompt\", options.prompt);\r\n }\r\n\r\n return `${authorizeEndpoint}?${params.toString()}`;\r\n }\r\n\r\n /**\r\n * 弹窗模式 SSO 登录\r\n *\r\n * 打开一个小窗口进行登录,认证完成后窗口自动关闭,\r\n * 主页面不丢失状态(适用于 SPA 中需要保持表单/浏览上下文的场景)。\r\n *\r\n * 流程:\r\n * 1. window.open() 打开授权 URL 到弹窗\r\n * 2. 用户在弹窗中完成登录\r\n * 3. 弹窗加载 CallbackPage 时检测到 window.opener,通过 postMessage 回传回调 URL\r\n * 4. 主页面收到消息后调用 handleCallback() 交换 token\r\n * 5. 弹窗自动关闭\r\n *\r\n * @param options - 弹窗配置\r\n * @param options.returnUrl - 登录成功后的返回地址\r\n * @param options.width - 弹窗宽度(默认 480)\r\n * @param options.height - 弹窗高度(默认 640)\r\n * @returns TokenData\r\n *\r\n * @example\r\n * ```typescript\r\n * const client = new SsoClient({ clientId: \"xxx\", redirectUri: \"https://myapp.com/callback\", ssoBaseUrl: \"https://nihplod.cn\" });\r\n * try {\r\n * const tokenData = await client.loginPopup({ returnUrl: \"/dashboard\" });\r\n * console.log(\"登录成功\", tokenData);\r\n * } catch (err) {\r\n * if (err instanceof SsoError && err.code === \"popup_blocked\") {\r\n * // 弹窗被拦截,回退到同页重定向\r\n * await client.login(\"/dashboard\");\r\n * }\r\n * }\r\n * ```\r\n */\r\n async loginPopup(\r\n options: { returnUrl?: string; width?: number; height?: number } = {}\r\n ): Promise<TokenData> {\r\n const verifier = generateCodeVerifier();\r\n const challenge = await generateCodeChallenge(verifier);\r\n const state = generateState();\r\n const nonce = generateState(); // OIDC nonce:回调时校验 ID Token 的 nonce claim\r\n const popupNonce = generateState(); // 独立的 nonce 用于 postMessage 来源验证\r\n\r\n savePkceVerifier(this.config.clientId, verifier);\r\n saveOAuthState(state, this.config.clientId);\r\n saveOAuthNonce(nonce, this.config.clientId);\r\n // 保存 popup nonce 到 sessionStorage,用于 postMessage 校验\r\n savePkceVerifier(`${this.config.clientId}_popup_nonce`, popupNonce);\r\n\r\n if (options.returnUrl) {\r\n this._saveReturnUrlIfTrusted(options.returnUrl);\r\n }\r\n\r\n const authorizeEndpoint = await this._getAuthorizeEndpoint();\r\n const params = new URLSearchParams();\r\n params.set(\"response_type\", \"code\");\r\n params.set(\"client_id\", this.config.clientId);\r\n params.set(\"redirect_uri\", this.config.redirectUri);\r\n params.set(\"scope\", this.config.scopes || \"openid profile\");\r\n params.set(\"state\", state);\r\n params.set(\"nonce\", nonce);\r\n params.set(\"code_challenge\", challenge);\r\n params.set(\"code_challenge_method\", \"S256\");\r\n // 弹窗 nonce:回调页通过 postMessage 回传,主窗口校验防伪造\r\n params.set(\"popup_nonce\", popupNonce);\r\n\r\n const width = options.width || 480;\r\n const height = options.height || 640;\r\n const left = Math.max(0, (window.screen.width - width) / 2);\r\n const top = Math.max(0, (window.screen.height - height) / 2);\r\n\r\n const popupFeatures = [\r\n `width=${width}`,\r\n `height=${height}`,\r\n `left=${Math.round(left)}`,\r\n `top=${Math.round(top)}`,\r\n \"resizable=yes\",\r\n \"scrollbars=yes\",\r\n \"status=yes\",\r\n ].join(\",\");\r\n\r\n const popup = window.open(\r\n `${authorizeEndpoint}?${params.toString()}`,\r\n \"nihplod_sso_popup\",\r\n popupFeatures\r\n );\r\n\r\n if (!popup) {\r\n removePkceVerifier(this.config.clientId);\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n throw new SsoError(\r\n \"popup_blocked\",\r\n \"弹窗被浏览器拦截,请允许弹窗后重试\"\r\n );\r\n }\r\n\r\n // 计算 redirect_uri 的 origin,用于 postMessage 来源校验\r\n const redirectUriOrigin = new URL(this.config.redirectUri).origin;\r\n\r\n try {\r\n // 聚焦弹窗\r\n popup.focus();\r\n\r\n return await new Promise<TokenData>((resolve, reject) => {\r\n let completed = false;\r\n\r\n const handleMessage = (event: MessageEvent) => {\r\n if (completed) return;\r\n // 校验消息来源:必须来自 redirect_uri 对应的子项目 origin(回调页运行位置)\r\n if (event.origin !== redirectUriOrigin) return;\r\n if (!event.data || event.data.type !== \"nihplod_sso_popup_callback\") return;\r\n if (!event.data.callbackUrl) return;\r\n\r\n // 校验 popup nonce:防止伪造的 postMessage(fail-closed:本地 nonce 缺失同样拒绝)\r\n const savedNonce = getPkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n if (!savedNonce || !timingSafeEqualString(event.data.nonce ?? \"\", savedNonce)) return;\r\n removePkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n\r\n // 回 ACK:回调页会每 500ms 重发直到收到 ACK(或超时提示手动关闭),\r\n // 防止主窗口监听未挂载时消息丢失导致弹窗悬挂\r\n try {\r\n (event.source as Window | null)?.postMessage(\r\n { type: \"nihplod_sso_popup_ack\", nonce: savedNonce },\r\n event.origin\r\n );\r\n } catch {\r\n // ACK 失败不影响主流程(弹窗会超时提示手动关闭)\r\n }\r\n\r\n completed = true;\r\n cleanup();\r\n\r\n if (popup && !popup.closed) {\r\n popup.close();\r\n }\r\n\r\n this.handleCallback(event.data.callbackUrl)\r\n .then((tokenData) => {\r\n // prompt=none 静默探测在弹窗模式不支持(loginPopup 不带 prompt),\r\n // null 结果视为探测无会话\r\n if (tokenData === null) {\r\n reject(\r\n new SsoError(\"not_authenticated\", \"SSO 会话不存在(静默探测无会话)\")\r\n );\r\n return;\r\n }\r\n resolve(tokenData);\r\n })\r\n .catch(reject);\r\n };\r\n\r\n const pollTimer = setInterval(() => {\r\n if (popup.closed) {\r\n if (!completed) {\r\n completed = true;\r\n cleanup();\r\n reject(\r\n new SsoError(\r\n \"popup_closed\",\r\n \"登录窗口已关闭\"\r\n )\r\n );\r\n }\r\n }\r\n }, 500);\r\n\r\n const cleanup = () => {\r\n clearInterval(pollTimer);\r\n window.removeEventListener(\"message\", handleMessage);\r\n removePkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n };\r\n\r\n window.addEventListener(\"message\", handleMessage);\r\n });\r\n } catch (err) {\r\n removePkceVerifier(this.config.clientId);\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n throw err;\r\n }\r\n }\r\n\r\n /**\r\n * 处理 OAuth 回调\r\n *\r\n * 解析回调 URL,校验 state 参数,用授权码交换 token。\r\n * 成功后 token 自动保存到 token 存储(默认 sessionStorage,可通过 setTokenStorage 定制)。\r\n *\r\n * @param callbackUrl - 完整的回调 URL(window.location.href)\r\n * @returns TokenData;prompt=none 静默探测且无 SSO 会话时返回 null\r\n * (IdP 回跳 login_required / consent_required / interaction_required\r\n * 且回调 state 与探测标记匹配),此时临时数据已清理,调用方按\"未登录\"处理即可\r\n */\r\n async handleCallback(\r\n callbackUrl: string\r\n ): Promise<TokenData | null> {\r\n const url = new URL(callbackUrl);\r\n const params = url.searchParams;\r\n\r\n // 检查错误(SSO 中心按 OAuth 2.0 规范回传 error 参数)\r\n const error = params.get(\"error\");\r\n if (error) {\r\n // 静默探测(prompt=none)无会话:IdP 以 error 回跳属预期结果而非异常。\r\n // 要求回调 state 与登录 state、探测标记三者一致(常量时间比较),\r\n // 防止伪造的 error 回调被当作探测结果\r\n const returnedErrorState = params.get(\"state\");\r\n const probeState = getSilentProbe(this.config.clientId);\r\n const savedStateForProbe = getOAuthState(this.config.clientId);\r\n if (\r\n SILENT_PROBE_ERRORS.has(error) &&\r\n probeState &&\r\n savedStateForProbe &&\r\n timingSafeEqualString(savedStateForProbe, returnedErrorState ?? \"\") &&\r\n timingSafeEqualString(probeState, returnedErrorState ?? \"\")\r\n ) {\r\n // 清除本次探测的全部临时数据(state/nonce/verifier/标记),返回 null\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n removeSilentProbe(this.config.clientId);\r\n return null;\r\n }\r\n // 授权已失败,本次流程的临时数据(state/verifier/nonce/探测标记)不再有用,一并清理避免残留\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n removeSilentProbe(this.config.clientId);\r\n const desc = params.get(\"error_description\") || error;\r\n throw new SsoError(mapOAuthErrorToSsoCode(error), `授权失败: ${desc}`);\r\n }\r\n\r\n const code = params.get(\"code\");\r\n const returnedState = params.get(\"state\");\r\n\r\n if (!code) {\r\n // 缺 code 的回调无法继续,清理临时数据避免残留\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n removeSilentProbe(this.config.clientId);\r\n throw new SsoError(\"token_request_failed\", \"回调 URL 中缺少 authorization code\");\r\n }\r\n\r\n // 校验 state 参数(CSRF 防护,常量时间比较)\r\n const savedState = getOAuthState(this.config.clientId);\r\n if (!savedState || !timingSafeEqualString(savedState, returnedState ?? \"\")) {\r\n // state 不匹配:state 与 verifier 一并清除,避免残留半套临时数据\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n removeSilentProbe(this.config.clientId);\r\n throw new SsoError(\r\n \"state_mismatch\",\r\n \"State 参数不匹配,可能存在 CSRF 攻击\"\r\n );\r\n }\r\n\r\n // state 校验通过:读取登录时保存的 OIDC nonce(validateIdToken 时 fail-closed 校验,\r\n // 成功后才随 state/verifier 一并清除——验签失败重试仍需它)\r\n const expectedNonce = getOAuthNonce(this.config.clientId) ?? undefined;\r\n\r\n // 获取 code_verifier\r\n const verifier = getPkceVerifier(this.config.clientId);\r\n if (!verifier) {\r\n throw new SsoError(\r\n \"pkce_required\",\r\n \"code_verifier 不存在(可能已过期或来自其他标签页)\"\r\n );\r\n }\r\n\r\n // 交换 token\r\n const tokenEndpoint = await this._getTokenEndpoint();\r\n const body = new URLSearchParams();\r\n body.set(\"grant_type\", \"authorization_code\");\r\n body.set(\"code\", code);\r\n body.set(\"code_verifier\", verifier);\r\n body.set(\"client_id\", this.config.clientId);\r\n body.set(\"redirect_uri\", this.config.redirectUri);\r\n if (this.config.clientSecret) {\r\n body.set(\"client_secret\", this.config.clientSecret);\r\n }\r\n\r\n let res: Response;\r\n try {\r\n res = await fetchWithTimeout(tokenEndpoint, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: body.toString(),\r\n });\r\n } catch (err) {\r\n throw new SsoError(\"network_error\", \"网络请求失败\", err);\r\n }\r\n\r\n if (!res.ok) {\r\n // 授权码已被消费(一次性),即使重试同一回调也只会再次得到 invalid_grant,\r\n // 因此与 error-param 路径一致清除本次流程的全部临时数据\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n removeSilentProbe(this.config.clientId);\r\n let errData: Record<string, unknown> = {};\r\n try { errData = await res.json(); } catch { /* ignore */ }\r\n const serverError = (errData.error as string) || \"\";\r\n throw new SsoError(\r\n mapOAuthErrorToSsoCode(serverError),\r\n (errData.error_description as string) || `Token 请求失败: HTTP ${res.status}`\r\n );\r\n }\r\n\r\n // 先解析响应体,成功后再清除 state 和 verifier:\r\n // 若 JSON 畸形导致解析抛错,保留 state/verifier 允许用户重试回调\r\n const data: TokenResponse = await res.json();\r\n\r\n // 防御性校验:refresh_token 缺失或 expires_in 非法时 fail-closed 抛错,\r\n // 不保存半截 token 数据(authorization_code 交换主站必然签发 refresh_token)\r\n assertValidTokenResponse(data, true);\r\n\r\n // OIDC:请求 scope 含 openid 时,token 响应必须包含 id_token。\r\n // 缺失时 nonce/at_hash/签名均无从校验,直接 fail-closed 拒绝而不是静默降级。\r\n const requestedScopes = (this.config.scopes || \"openid profile\").split(\" \").filter(Boolean);\r\n if (requestedScopes.includes(\"openid\") && !data.id_token) {\r\n // 授权码已消费、重试必然得到相同结果,临时数据(state/verifier/nonce)一并清除\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n removeSilentProbe(this.config.clientId);\r\n removeTokenData(this.config.clientId);\r\n throw new SsoError(\r\n \"id_token_invalid\",\r\n \"Token 响应缺少 id_token(scope 含 openid 时必需)\"\r\n );\r\n }\r\n\r\n // OIDC:验证 ID Token 签名、基本声明及 at_hash\r\n if (data.id_token) {\r\n try {\r\n await validateIdToken(\r\n data.id_token,\r\n data.access_token,\r\n this.config.ssoBaseUrl,\r\n this.config.clientId,\r\n { expectedNonce }\r\n );\r\n } catch (err) {\r\n // 验证失败:不保存任何 token,防止伪造 ID Token\r\n removeTokenData(this.config.clientId);\r\n throw err;\r\n }\r\n }\r\n\r\n // 全部校验通过后清除 state、nonce 和 verifier(一次性临时数据)。\r\n // 推迟到验签之后:若 JWKS 暂时不可达导致验签失败,\r\n // 保留 state/verifier/nonce,用户刷新回调页即可重试\r\n removeOAuthState(this.config.clientId);\r\n removeOAuthNonce(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n removeSilentProbe(this.config.clientId);\r\n\r\n const now = Date.now();\r\n const tokenData: TokenData = {\r\n access_token: data.access_token,\r\n token_type: data.token_type,\r\n expires_in: data.expires_in,\r\n refresh_token: data.refresh_token,\r\n id_token: data.id_token,\r\n issued_at: now,\r\n expires_at: now + data.expires_in * 1000,\r\n };\r\n\r\n saveTokenData(tokenData, this.config.clientId);\r\n this.debugLog(\"handleCallback: token 交换成功\", { clientId: this.config.clientId });\r\n return tokenData;\r\n }\r\n\r\n /**\r\n * 刷新 Access Token\r\n *\r\n * 使用 refresh_token 换取新的 access_token。\r\n * 采用互斥锁防止并发刷新。\r\n * 支持 Refresh Token 原子轮换。\r\n */\r\n async refreshToken(): Promise<TokenData> {\r\n // 互斥锁:避免并发刷新\r\n if (this._refreshLock) return this._refreshLock;\r\n\r\n this._refreshLock = this._doRefreshToken();\r\n try {\r\n return await this._refreshLock;\r\n } finally {\r\n this._refreshLock = null;\r\n }\r\n }\r\n\r\n private async _doRefreshToken(): Promise<TokenData> {\r\n const current = getTokenData(this.config.clientId);\r\n if (!current?.refresh_token) {\r\n throw new SsoError(\"no_refresh_token\", \"没有可用的 refresh_token\");\r\n }\r\n\r\n const tokenEndpoint = await this._getTokenEndpoint();\r\n const body = new URLSearchParams();\r\n body.set(\"grant_type\", \"refresh_token\");\r\n body.set(\"refresh_token\", current.refresh_token);\r\n body.set(\"client_id\", this.config.clientId);\r\n if (this.config.clientSecret) {\r\n body.set(\"client_secret\", this.config.clientSecret);\r\n }\r\n\r\n let res: Response | null = null;\r\n let lastErr: unknown;\r\n for (let attempt = 0; attempt < 2; attempt++) {\r\n try {\r\n res = await fetchWithTimeout(tokenEndpoint, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: body.toString(),\r\n });\r\n lastErr = null;\r\n break;\r\n } catch (err) {\r\n lastErr = err;\r\n if (attempt < 1) await new Promise((r) => setTimeout(r, 1000));\r\n }\r\n }\r\n if (lastErr || !res) {\r\n throw new SsoError(\"network_error\", \"刷新 Token 网络请求失败\", lastErr);\r\n }\r\n\r\n if (!res.ok) {\r\n let errData: Record<string, unknown> = {};\r\n try { errData = await res.json(); } catch { /* ignore */ }\r\n const errorCode = (errData.error as string) || \"\";\r\n // 仅明确的 invalid_grant(refresh_token 已被撤销/过期)才清除本地登录态;\r\n // 网关/WAF 返回的 401(响应体无 OAuth error 字段)保留 token 按可重试处理,\r\n // 避免一次网络抖动把用户静默登出(重试上限由上层 SsoProvider 计数控制)\r\n if (errorCode === \"invalid_grant\") {\r\n removeTokenData(this.config.clientId);\r\n }\r\n this.debugLog(\"refreshToken: 刷新失败\", { status: res.status, error: errorCode });\r\n if (!errorCode && res.status === 401) {\r\n throw new SsoError(\"sso_server_error\", \"刷新 Token 失败(网关异常),请稍后重试\");\r\n }\r\n throw new SsoError(\r\n mapOAuthErrorToSsoCode(errorCode, \"refresh\"),\r\n (errData.error_description as string) || `刷新 Token 失败: HTTP ${res.status}`\r\n );\r\n }\r\n\r\n const data: TokenResponse = await res.json();\r\n\r\n // 防御性校验:refresh 响应可省略 refresh_token(沿用旧值),\r\n // 但 access_token / expires_in 必须合法,否则 expires_at 为 NaN 永久判过期\r\n assertValidTokenResponse(data, false);\r\n\r\n if (data.id_token) {\r\n try {\r\n // refresh 流程不传 expectedNonce:原始 nonce 是一次性的,登录成功后已删除。\r\n // refresh 签发的 ID Token 按 OIDC Core §12.2 不得携带 nonce;\r\n // validateIdToken 现已 fail-closed 拒绝任何携带 nonce 但无期望值的 token,\r\n // 因此此处无需传值也不会误伤合法 refresh 响应\r\n await validateIdToken(\r\n data.id_token,\r\n data.access_token,\r\n this.config.ssoBaseUrl,\r\n this.config.clientId\r\n );\r\n } catch (err) {\r\n removeTokenData(this.config.clientId);\r\n throw err;\r\n }\r\n }\r\n\r\n const now = Date.now();\r\n const tokenData: TokenData = {\r\n access_token: data.access_token,\r\n token_type: data.token_type,\r\n expires_in: data.expires_in,\r\n // RFC 6749 §6:刷新响应可省略 refresh_token,此时沿用旧值\r\n refresh_token: data.refresh_token || current.refresh_token,\r\n id_token: data.id_token,\r\n issued_at: now,\r\n expires_at: now + data.expires_in * 1000,\r\n };\r\n\r\n saveTokenData(tokenData, this.config.clientId);\r\n return tokenData;\r\n }\r\n\r\n /**\r\n * 获取用户信息\r\n *\r\n * 若 access_token 已过期则自动刷新后再请求。\r\n */\r\n async getUserInfo(): Promise<SsoUser> {\r\n let tokenData = getTokenData(this.config.clientId);\r\n\r\n if (!tokenData) {\r\n throw new SsoError(\"not_authenticated\", \"未登录\");\r\n }\r\n\r\n // 若已过期(含 10s 提前量:避免返回即将过期的 token),先刷新\r\n if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {\r\n tokenData = await this.refreshToken();\r\n }\r\n\r\n const userinfoEndpoint = await this._getUserinfoEndpoint();\r\n\r\n let res: Response;\r\n try {\r\n res = await fetchWithTimeout(userinfoEndpoint, {\r\n headers: {\r\n Authorization: `Bearer ${tokenData.access_token}`,\r\n },\r\n });\r\n } catch (err) {\r\n throw new SsoError(\"network_error\", \"获取用户信息网络请求失败\", err);\r\n }\r\n\r\n if (!res.ok) {\r\n if (res.status === 401) {\r\n removeTokenData(this.config.clientId);\r\n throw new SsoError(\"not_authenticated\", \"Token 已失效\");\r\n }\r\n throw new SsoError(\r\n \"userinfo_failed\",\r\n `获取用户信息失败: HTTP ${res.status}`\r\n );\r\n }\r\n\r\n return (await res.json()) as SsoUser;\r\n }\r\n\r\n /**\r\n * 获取当前 access_token\r\n *\r\n * 若已过期则自动刷新。用于子项目自行发起 API 请求时获取 Bearer token。\r\n * 若无 token 返回 null;若刷新失败则抛出错误(与 getUserInfo 行为一致)。\r\n */\r\n async getAccessToken(): Promise<string | null> {\r\n let tokenData = getTokenData(this.config.clientId);\r\n if (!tokenData) return null;\r\n\r\n if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {\r\n tokenData = await this.refreshToken();\r\n }\r\n\r\n return tokenData.access_token;\r\n }\r\n\r\n /**\r\n * 检查是否已认证(不发起网络请求)\r\n *\r\n * 仅检查本地是否存在未过期的 access_token。\r\n * 故意不做 EXPIRY_SKEW_MS 提前量:这是同步的 UI 提示判定(是否显示\"已登录\"),\r\n * 提前 10s 翻转 false 会让调用方在 token 仍有效时误显示未登录;真正发请求的\r\n * getAccessToken/getUserInfo 已带提前量。\r\n */\r\n isAuthenticated(): boolean {\r\n const tokenData = getTokenData(this.config.clientId);\r\n if (!tokenData) return false;\r\n return Date.now() < tokenData.expires_at;\r\n }\r\n\r\n /**\r\n * 登出\r\n *\r\n * 清除本地所有 token 和临时数据,并尝试撤销服务端 refresh_token。\r\n * @param redirectToSso - 是否重定向到 SSO 登出页(默认 false)。\r\n * 为 true 时携带 state 参数(已保存到 sessionStorage),\r\n * 回跳页面应调用 validateLogoutState() 校验防登出 CSRF。\r\n */\r\n async logout(redirectToSso: boolean = false): Promise<void> {\r\n // 获取 refresh_token 用于服务端撤销(需在 clearAllSsoData 之前)\r\n const tokenData = getTokenData(this.config.clientId);\r\n const refreshToken = tokenData?.refresh_token;\r\n const idTokenHint = tokenData?.id_token;\r\n\r\n clearAllSsoData(this.config.clientId);\r\n this.debugLog(\"logout: 本地数据已清除\", {\r\n clientId: this.config.clientId,\r\n redirectToSso,\r\n });\r\n\r\n // 尝试调用服务端 token revocation 端点(best-effort)\r\n // 仅 Confidential Client 携带 client_secret;Public Client 不传 secret。\r\n if (refreshToken && this.config.clientId) {\r\n try {\r\n // serverBaseUrl 配置时直连内网默认端点(discovery 文档内是公网 URL,\r\n // 会绕行公网);否则用 Discovery,兜底公网默认路径\r\n const discovery = this.config.serverBaseUrl ? null : await this._getDiscovery();\r\n const revokeUrl = this.config.serverBaseUrl\r\n ? `${this._serverBase}/api/oauth/revoke`\r\n : discovery?.revocation_endpoint ||\r\n `${this.config.ssoBaseUrl}/api/oauth/revoke`;\r\n const revokeBody = new URLSearchParams({\r\n token: refreshToken,\r\n token_type_hint: \"refresh_token\",\r\n client_id: this.config.clientId,\r\n });\r\n if (this.config.clientSecret) {\r\n revokeBody.set(\"client_secret\", this.config.clientSecret);\r\n }\r\n await fetchWithTimeout(\r\n revokeUrl,\r\n {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: revokeBody.toString(),\r\n },\r\n REVOKE_TIMEOUT_MS\r\n );\r\n } catch {\r\n // 撤销失败不影响本地登出(best-effort)\r\n }\r\n }\r\n\r\n if (redirectToSso) {\r\n // OIDC RP-Initiated Logout:携带 client_id / post_logout_redirect_uri / id_token_hint / state\r\n const discovery = await this._getDiscovery();\r\n const endSessionEndpoint =\r\n discovery?.end_session_endpoint || `${this.config.ssoBaseUrl}/api/oauth/end-session`;\r\n const logoutUrl = new URL(endSessionEndpoint);\r\n logoutUrl.searchParams.set(\"client_id\", this.config.clientId);\r\n const postLogoutUri = this.config.postLogoutRedirectUri || this.config.redirectUri;\r\n if (postLogoutUri) {\r\n logoutUrl.searchParams.set(\r\n \"post_logout_redirect_uri\",\r\n postLogoutUri\r\n );\r\n }\r\n if (idTokenHint) {\r\n logoutUrl.searchParams.set(\"id_token_hint\", idTokenHint);\r\n }\r\n // 携带 state 防登出 CSRF:回跳时用 validateLogoutState() 校验\r\n const state = generateState();\r\n saveLogoutState(state, this.config.clientId);\r\n logoutUrl.searchParams.set(\"state\", state);\r\n window.location.href = logoutUrl.toString();\r\n }\r\n }\r\n\r\n /**\r\n * 校验 RP-Initiated Logout 回跳的 state 参数(登出 CSRF 防护)\r\n *\r\n * 在 post_logout_redirect_uri 指向的页面加载时调用;\r\n * 仅在 URL 携带 state 且与 logout(redirectToSso=true) 保存的值一致时返回 true,\r\n * 校验后清除已保存的 logout state(一次性)。\r\n *\r\n * @param url - 当前页面完整 URL(window.location.href)\r\n *\r\n * @example\r\n * ```typescript\r\n * if (sso.validateLogoutState(window.location.href)) {\r\n * // 来自 SSO 登出的可信回跳\r\n * }\r\n * ```\r\n */\r\n validateLogoutState(url: string): boolean {\r\n const returnedState = new URL(url).searchParams.get(\"state\");\r\n if (!returnedState) return false;\r\n const savedState = getLogoutState(this.config.clientId);\r\n if (!savedState) return false;\r\n if (!timingSafeEqualString(savedState, returnedState)) return false;\r\n // 校验通过:logout state 为一次性临时数据,立即清除\r\n removeLogoutState(this.config.clientId);\r\n return true;\r\n }\r\n\r\n /**\r\n * 获取 OIDC Discovery 文档\r\n *\r\n * 用于调试和获取 SSO 中心完整配置。\r\n * 可能返回 null(当 Discovery 端点不可达且无缓存时)。\r\n */\r\n async getDiscovery(): Promise<OidcDiscovery | null> {\r\n return this._getDiscovery();\r\n }\r\n}\r\n","/**\r\n * RequireAuth / withAuth — 路由保护组件\r\n *\r\n * 用法:\r\n * - <RequireAuth> 包裹需要登录才能访问的路由\r\n * - withAuth(Component) HOC 包装单个页面组件\r\n */\r\n\r\n\"use client\";\r\n\r\nimport React, { useEffect, useRef, useState, type ComponentType } from \"react\";\r\nimport { useSso } from \"./SsoProvider\";\r\n\r\n// ============================================\r\n// RequireAuth 组件\r\n// ============================================\r\n\r\nexport interface RequireAuthProps {\r\n /** 子组件(受保护的内容) */\r\n children: React.ReactNode;\r\n\r\n /** 未登录时的回退内容(默认显示加载中) */\r\n fallback?: React.ReactNode;\r\n\r\n /** 是否在检测到未登录时自动发起登录跳转 */\r\n autoLogin?: boolean;\r\n\r\n /**\r\n * 是否使用弹窗模式登录(保持当前页面状态不丢失)\r\n *\r\n * 仅在 autoLogin=true 时生效。弹窗被拦截时自动回退到同页重定向。\r\n */\r\n usePopup?: boolean;\r\n\r\n /**\r\n * 登录发起失败时的回调(如弹窗被关闭 popup_closed)。\r\n * 失败后组件会展示重试入口,不会永久停在\"请先登录\"。\r\n */\r\n onError?: (error: unknown) => void;\r\n\r\n /**\r\n * 自定义登录失败 UI。不传时显示默认的错误提示与\"重试\"按钮。\r\n * retry() 会重新发起登录。\r\n */\r\n renderLoginError?: (error: unknown, retry: () => void) => React.ReactNode;\r\n}\r\n\r\n/**\r\n * 路由保护组件\r\n *\r\n * 未登录时自动触发登录跳转或显示回退内容。\r\n * 登录发起失败(如弹窗被关闭)时显示重试入口,并通过 onError 上报。\r\n *\r\n * @example\r\n * ```tsx\r\n * // 弹窗模式:不中断用户当前操作\r\n * <RequireAuth autoLogin usePopup>\r\n * <Dashboard />\r\n * </RequireAuth>\r\n * ```\r\n */\r\nexport function RequireAuth({\r\n children,\r\n fallback,\r\n autoLogin = true,\r\n usePopup = false,\r\n onError,\r\n renderLoginError,\r\n}: RequireAuthProps) {\r\n const { isAuthenticated, isLoading, login, loginPopup } = useSso();\r\n const loginTriggeredRef = useRef(false);\r\n const [loginError, setLoginError] = useState<unknown>(null);\r\n\r\n const triggerLogin = () => {\r\n loginTriggeredRef.current = true;\r\n const currentPath = window.location.pathname + window.location.search;\r\n\r\n const handleFailure = (err: unknown) => {\r\n // 失败时重置触发标记,允许重试;否则组件会永久停在\"请先登录\"\r\n loginTriggeredRef.current = false;\r\n setLoginError(err);\r\n onError?.(err);\r\n };\r\n\r\n if (usePopup) {\r\n loginPopup({ returnUrl: currentPath }).catch((err) => {\r\n // 弹窗被拦截时回退到同页重定向\r\n if ((err as { code?: string }).code === \"popup_blocked\") {\r\n login(currentPath).catch(handleFailure);\r\n return;\r\n }\r\n // popup_closed 等其他错误:展示重试入口\r\n handleFailure(err);\r\n });\r\n } else {\r\n login(currentPath).catch(handleFailure);\r\n }\r\n };\r\n\r\n const retry = () => {\r\n setLoginError(null);\r\n triggerLogin();\r\n };\r\n\r\n useEffect(() => {\r\n if (!isLoading && !isAuthenticated && autoLogin && !loginTriggeredRef.current) {\r\n triggerLogin();\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [isLoading, isAuthenticated, autoLogin, usePopup, login, loginPopup]);\r\n\r\n if (isLoading) {\r\n return (\r\n fallback || React.createElement(\"div\", null, \"正在验证登录状态...\")\r\n );\r\n }\r\n\r\n if (!isAuthenticated) {\r\n if (loginError) {\r\n if (renderLoginError) {\r\n return React.createElement(\r\n React.Fragment,\r\n null,\r\n renderLoginError(loginError, retry)\r\n );\r\n }\r\n const message =\r\n loginError instanceof Error ? loginError.message : \"登录失败\";\r\n return React.createElement(\r\n \"div\",\r\n null,\r\n React.createElement(\"p\", null, `登录未完成:${message}`),\r\n React.createElement(\"button\", { onClick: retry }, \"重试登录\")\r\n );\r\n }\r\n if (fallback) return React.createElement(React.Fragment, null, fallback);\r\n return React.createElement(\"div\", null, \"请先登录\");\r\n }\r\n\r\n return React.createElement(React.Fragment, null, children);\r\n}\r\n\r\n// ============================================\r\n// withAuth HOC\r\n// ============================================\r\n\r\n/**\r\n * 高阶组件:包装页面组件,要求认证后才能访问\r\n *\r\n * @example\r\n * ```tsx\r\n * function DashboardPage() { return <div>Dashboard</div>; }\r\n * export default withAuth(DashboardPage);\r\n * ```\r\n */\r\nexport function withAuth<P extends object>(\r\n Component: ComponentType<P>\r\n): ComponentType<P> {\r\n const displayName =\r\n Component.displayName || Component.name || \"Component\";\r\n\r\n function WrappedComponent(props: P) {\r\n return React.createElement(\r\n RequireAuth,\r\n null,\r\n React.createElement(Component, props)\r\n );\r\n }\r\n\r\n WrappedComponent.displayName = `withAuth(${displayName})`;\r\n return WrappedComponent;\r\n}\r\n","/**\n * CallbackPage — 通用 OAuth 回调页面组件\n *\n * 子项目只需在回调路由渲染此组件即可完成 code → token 交换。\n * 交换成功后默认整页跳转到 returnUrl 或首页;\n * 传入 onSuccess 可跳过默认跳转,由 SPA 路由接管(保持应用状态)。\n *\n * @example\n * ```tsx\n * // 在回调页面路由中:\n * import { CallbackPage } from \"@nihplod/sso-sdk/react\";\n * export default function AuthCallback() {\n * return <CallbackPage />;\n * }\n * ```\n *\n * @example SPA 路由接管跳转(不整页刷新):\n * ```tsx\n * <CallbackPage onSuccess={() => navigate(\"/dashboard\", { replace: true })} />\n * ```\n */\n\n\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { useSso } from \"./SsoProvider\";\nimport { getReturnUrl, removeReturnUrl, type TokenData } from \"../core/storage\";\nimport { isTrustedReturnUrl } from \"../core/security\";\nimport { SsoError } from \"../core/errors\";\n\nexport interface CallbackPageProps {\n /**\n * 登录成功回调。传入后跳过默认的整页跳转(window.location.href),\n * 由调用方用 SPA 路由接管跳转,避免丢失应用内状态。\n */\n onSuccess?: (tokenData: TokenData) => void;\n\n /** 登录失败回调(错误同时会展示在错误页,除非提供了 renderError) */\n onError?: (error: Error) => void;\n\n /**\n * 自定义错误页渲染。不传时使用默认错误 UI(DefaultCallbackError)。\n */\n renderError?: (error: string) => React.ReactNode;\n}\n\n/** 将 SDK 错误码映射为用户可读文案(避免向终端用户暴露 state/CSRF/PKCE 等技术术语) */\nconst CALLBACK_ERROR_HINTS: Partial<Record<string, string>> = {\n state_mismatch: \"登录会话校验失败,请重新登录\",\n pkce_required: \"登录会话不完整,请重新登录\",\n user_denied_authorization: \"你已取消登录\",\n session_expired: \"登录已过期,请重新登录\",\n authorization_code_expired: \"登录信息已过期,请重新登录\",\n authorization_code_used: \"登录信息已被使用,请重新登录\",\n token_request_failed: \"登录失败,请重新登录\",\n userinfo_failed: \"登录信息获取失败,请重试\",\n not_authenticated: \"请先登录后再访问\",\n network_error: \"网络异常,请检查网络后重试\",\n rate_limited: \"操作过于频繁,请稍后重试\",\n popup_blocked: \"登录窗口被浏览器拦截,请允许弹窗后重试\",\n popup_closed: \"登录窗口已关闭,请重新登录\",\n client_disabled: \"该应用已停用,请联系管理员\",\n account_disabled: \"账号暂时无法使用,请联系客服\",\n sso_server_error: \"登录服务暂时不可用,请稍后重试\",\n invalid_config: \"应用配置有误,请联系管理员\",\n no_refresh_token: \"登录已过期,请重新登录\",\n};\n\n/** 统一将回调异常转换为面向用户的中文提示 */\nexport function getCallbackErrorMessage(err: unknown): string {\n if (err instanceof SsoError) {\n const hint = CALLBACK_ERROR_HINTS[err.code];\n if (hint) return hint;\n if (err.code.startsWith(\"id_token\")) return \"登录校验失败,请重新登录\";\n // 描述为中文(业务可控文案)时直接展示,否则回退通用文案\n if (/[\\u4e00-\\u9fff]/.test(err.description)) return err.description;\n return \"登录失败,请重新登录\";\n }\n return \"登录失败,请重试\";\n}\n\n/** 默认错误页 UI(可通过 renderError 完全替换) */\nexport function DefaultCallbackError({ error }: { error: string }) {\n return React.createElement(\n \"div\",\n {\n style: {\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: \"100vh\",\n fontFamily: \"system-ui, sans-serif\",\n padding: \"2rem\",\n textAlign: \"center\",\n },\n },\n React.createElement(\n \"p\",\n { style: { color: \"#dc2626\", marginBottom: \"1.5rem\" } },\n error\n ),\n React.createElement(\n \"div\",\n { style: { display: \"flex\", gap: \"0.75rem\" } },\n React.createElement(\n \"a\",\n {\n href: \"/\",\n style: {\n display: \"inline-block\",\n padding: \"0.6rem 1.4rem\",\n background: \"#2c2c2c\",\n color: \"#fff\",\n fontSize: \"0.8125rem\",\n textDecoration: \"none\",\n },\n },\n \"重新登录\"\n ),\n React.createElement(\n \"button\",\n {\n type: \"button\",\n onClick: () => {\n if (window.history.length > 1) window.history.back();\n else window.location.href = \"/\";\n },\n style: {\n display: \"inline-block\",\n padding: \"0.6rem 1.4rem\",\n border: \"1px solid rgba(44, 44, 44, 0.25)\",\n background: \"transparent\",\n color: \"#2c2c2c\",\n fontSize: \"0.8125rem\",\n cursor: \"pointer\",\n },\n },\n \"返回上一页\"\n )\n )\n );\n}\n\nexport function CallbackPage({ onSuccess, onError, renderError }: CallbackPageProps = {}) {\n const { client, refreshUser } = useSso();\n const [error, setError] = useState<string | null>(null);\n const [processing, setProcessing] = useState(true);\n // 弹窗模式:postMessage 已发送但超时未收到主窗口 ACK(主窗口监听未挂载等),\n // 提示用户手动关闭窗口,避免弹窗悬挂\n const [awaitingManualClose, setAwaitingManualClose] = useState(false);\n\n // useLatest 惯例:用 ref 保存最新的 onSuccess/onError,\n // 避免调用方传内联回调时父组件重渲染导致下方 effect 重跑、\n // 已消费的 code 被重复交换 token(闪现假错误页)。\n const onSuccessRef = useRef(onSuccess);\n const onErrorRef = useRef(onError);\n useEffect(() => {\n onSuccessRef.current = onSuccess;\n onErrorRef.current = onError;\n });\n\n // React StrictMode 开发模式下 effect 会执行两次(mount → cleanup → mount)。\n // 授权码只能消费一次:用共享的 in-flight Promise 保证只交换一次;\n // 第二次挂载复用同一 Promise 继续做后处理(跳转/onSuccess),\n // 避免第一次运行被 cleanup 标记取消后整个流程停在 loading。\n const callbackPromiseRef = useRef<Promise<TokenData | null> | null>(null);\n // 后处理(跳转 / onSuccess / 错误展示)只执行一次,防止 effect 因依赖变化重跑时重复执行\n const postProcessedRef = useRef(false);\n const errorHandledRef = useRef(false);\n\n useEffect(() => {\n // 弹窗模式:仅在携带 popup_nonce 时启用(loginPopup 生成并回传)。\n // 无 nonce 时按普通回调处理,避免被同源页面以 window.open 打开时悬挂。\n const popupNonce = new URL(window.location.href).searchParams.get(\"popup_nonce\");\n if (window.opener && !window.opener.closed && popupNonce) {\n const nonce = popupNonce;\n // opener 与弹窗同源时可直接读 origin;跨源访问会抛 SecurityError,回退到当前 origin\n let targetOrigin = window.location.origin;\n try {\n targetOrigin = window.opener.location.origin;\n } catch {\n // 跨源 opener:使用当前 origin(postMessage 会校验,不会泄露给第三方)\n }\n\n // postMessage 只发一次可能赶上主窗口监听未挂载而丢失;\n // 每 500ms 重发直到收到主窗口 ACK,超时 10s 提示手动关闭窗口\n const message = {\n type: \"nihplod_sso_popup_callback\",\n callbackUrl: window.location.href,\n nonce,\n };\n let acked = false;\n const send = () => {\n if (acked || !window.opener || window.opener.closed) return;\n window.opener.postMessage(message, targetOrigin);\n };\n send();\n const resendTimer = setInterval(send, 500);\n const ackTimeout = setTimeout(() => {\n if (acked) return;\n clearInterval(resendTimer);\n setAwaitingManualClose(true);\n }, 10_000);\n\n const handleAck = (event: MessageEvent) => {\n // origin / 来源窗口 / nonce 三重校验,与主窗口侧对称\n if (event.source !== window.opener) return;\n if (event.origin !== targetOrigin) return;\n if (!event.data || event.data.type !== \"nihplod_sso_popup_ack\") return;\n if (event.data.nonce !== nonce) return;\n acked = true;\n clearInterval(resendTimer);\n clearTimeout(ackTimeout);\n };\n window.addEventListener(\"message\", handleAck);\n\n // 微任务延迟,避免 effect 内同步 setState\n Promise.resolve().then(() => setProcessing(false));\n\n return () => {\n clearInterval(resendTimer);\n clearTimeout(ackTimeout);\n window.removeEventListener(\"message\", handleAck);\n };\n }\n\n let cancelled = false;\n\n async function handleCallback() {\n try {\n if (!callbackPromiseRef.current) {\n callbackPromiseRef.current = client.handleCallback(window.location.href);\n }\n const tokenData = await callbackPromiseRef.current;\n\n if (cancelled || postProcessedRef.current) return;\n postProcessedRef.current = true;\n\n // 静默探测(prompt=none)无 SSO 会话:handleCallback 返回 null。\n // 跳回 returnUrl 并附 sso_probe=no_session,子站据此按\"未登录\"展示\n if (tokenData === null) {\n const clientId = client.config.clientId;\n const probeReturnUrl = getReturnUrl(clientId);\n removeReturnUrl(clientId);\n const target =\n probeReturnUrl && isTrustedReturnUrl(probeReturnUrl, window.location.origin)\n ? probeReturnUrl\n : \"/\";\n window.location.href =\n target + (target.includes(\"?\") ? \"&\" : \"?\") + \"sso_probe=no_session\";\n return;\n }\n\n // 刷新用户信息\n await refreshUser();\n\n if (cancelled) return;\n\n // 读取并清除 returnUrl(按 clientId 隔离,与 login() 写入的 key 对应)\n const clientId = client.config.clientId;\n const returnUrl = getReturnUrl(clientId);\n removeReturnUrl(clientId);\n\n // 传入 onSuccess 时由调用方接管跳转(SPA 路由),跳过默认整页跳转\n const onSuccessCb = onSuccessRef.current;\n if (onSuccessCb) {\n onSuccessCb(tokenData);\n return;\n }\n\n // 默认:整页跳转到 returnUrl 或首页(开放重定向防护:仅相对路径或同源)\n // token 默认存 sessionStorage,整页跳转后登录态仍保留\n window.location.href =\n returnUrl && isTrustedReturnUrl(returnUrl, window.location.origin)\n ? returnUrl\n : \"/\";\n } catch (err) {\n if (cancelled || errorHandledRef.current) return;\n errorHandledRef.current = true;\n // 失败后清空 Promise,允许显式重试(如组件重新挂载且授权码未被消费的场景)\n callbackPromiseRef.current = null;\n const errorObj = err instanceof Error ? err : new Error(String(err));\n onErrorRef.current?.(errorObj);\n setError(getCallbackErrorMessage(errorObj));\n setProcessing(false);\n }\n }\n\n handleCallback();\n\n return () => { cancelled = true; };\n }, [client, refreshUser]);\n\n if (error) {\n if (renderError) return React.createElement(React.Fragment, null, renderError(error));\n return React.createElement(DefaultCallbackError, { error });\n }\n\n if (awaitingManualClose) {\n return React.createElement(\n \"div\",\n {\n style: {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: \"100vh\",\n fontFamily: \"system-ui, sans-serif\",\n },\n },\n React.createElement(\"p\", null, \"登录已完成,请手动关闭此窗口\")\n );\n }\n\n if (processing) {\n return React.createElement(\n \"div\",\n {\n style: {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n minHeight: \"100vh\",\n fontFamily: \"system-ui, sans-serif\",\n },\n },\n React.createElement(\"p\", null, \"正在处理登录回调...\")\n );\n }\n\n return null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACWA,mBAQO;;;ACmCA,SAAS,uBACd,YACA,UAAwC,kBAC1B;AACd,UAAQ,YAAY;AAAA,IAClB,KAAK;AAEH,aAAO,YAAY,YAAY,oBAAoB;AAAA,IACrD,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAEH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EACX;AACF;AAKO,IAAM,WAAN,cAAuB,MAAM;AAAA,EAKlC,YAAY,MAAoB,aAAqB,OAAiB;AACpE,UAAM,aAAa,IAAI,KAAK,WAAW,EAAE;AACzC,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,cAAc;AACnB,SAAK,QAAQ;AAAA,EACf;AACF;;;ACrFA,IAAM,cACJ;AASF,SAAS,qBAAqB,QAAwB;AACpD,QAAM,OAAO;AACb,MAAI,SAAS;AAEb,SAAO,OAAO,SAAS,QAAQ;AAE7B,UAAM,YAAY,KAAK,IAAI,SAAS,GAAG,GAAG;AAC1C,UAAM,QAAQ,IAAI,WAAW,SAAS;AACtC,WAAO,gBAAgB,KAAK;AAE5B,aAAS,IAAI,GAAG,IAAI,aAAa,OAAO,SAAS,QAAQ,KAAK;AAC5D,UAAI,MAAM,CAAC,KAAK,KAAM;AACtB,gBAAU,YAAY,MAAM,CAAC,IAAI,YAAY,MAAM;AAAA,IACrD;AAAA,EACF;AAEA,SAAO;AACT;AAUO,SAAS,qBAAqB,SAAiB,IAAY;AAChE,MAAI,SAAS,MAAM,SAAS,KAAK;AAC/B,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AACA,SAAO,qBAAqB,MAAM;AACpC;AASA,eAAsB,sBACpB,cACiB;AACjB,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,QAAQ,OAAO,YAAY;AACxC,QAAM,OAAO,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI;AAGvD,QAAM,QAAQ,IAAI,WAAW,IAAI;AACjC,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,MAAM,YAAY,KAAK;AACzC,cAAU,OAAO,aAAa,MAAM,CAAC,CAAC;AAAA,EACxC;AACA,SAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC/E;AAOO,SAAS,gBAAwB;AACtC,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,SAAO,gBAAgB,KAAK;AAC5B,SAAO,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC1E;;;ACvCA,IAAM,iBAAiB;AAGvB,IAAM,YAAY;AAClB,IAAM,sBAAsB;AAC5B,IAAM,YAAY;AAClB,IAAM,YAAY;AAClB,IAAM,iBAAiB;AACvB,IAAM,mBAAmB;AAEzB,IAAM,mBAAmB;AAEzB,SAAS,SAAS,MAAc,UAA2B;AACzD,SAAO,WAAW,GAAG,IAAI,IAAI,QAAQ,KAAK;AAC5C;AAUA,SAAS,6BAA2C;AAClD,QAAM,QAAQ,oBAAI,IAAoB;AACtC,SAAO;AAAA,IACL,IAAI,KAAa;AACf,aAAO,MAAM,IAAI,GAAG,KAAK;AAAA,IAC3B;AAAA,IACA,IAAI,KAAa,OAAe;AAC9B,YAAM,IAAI,KAAK,KAAK;AAAA,IACtB;AAAA,IACA,OAAO,KAAa;AAClB,YAAM,OAAO,GAAG;AAAA,IAClB;AAAA,IACA,OAAO;AACL,aAAO,CAAC,GAAG,MAAM,KAAK,CAAC;AAAA,IACzB;AAAA,EACF;AACF;AAEA,IAAM,uBAAuB,2BAA2B;AAmExD,SAAS,8BAA4C;AACnD,QAAM,WAAW,oBAAI,IAAoB;AACzC,SAAO;AAAA,IACL,IAAI,KAAa;AACf,UAAI;AACF,YAAI,OAAO,mBAAmB,aAAa;AACzC,iBAAO,eAAe,QAAQ,iBAAiB,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK;AAAA,QAC9E;AAAA,MACF,QAAQ;AAAA,MAER;AACA,aAAO,SAAS,IAAI,GAAG,KAAK;AAAA,IAC9B;AAAA,IACA,IAAI,KAAa,OAAe;AAC9B,UAAI;AACF,YAAI,OAAO,mBAAmB,aAAa;AACzC,yBAAe,QAAQ,iBAAiB,KAAK,KAAK;AAClD;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AACA,eAAS,IAAI,KAAK,KAAK;AAAA,IACzB;AAAA,IACA,OAAO,KAAa;AAClB,eAAS,OAAO,GAAG;AACnB,UAAI;AACF,YAAI,OAAO,mBAAmB,YAAa;AAC3C,uBAAe,WAAW,iBAAiB,GAAG;AAAA,MAChD,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IACA,OAAO;AACL,YAAM,SAAS,IAAI,IAAY,SAAS,KAAK,CAAC;AAC9C,UAAI;AACF,YAAI,OAAO,mBAAmB,aAAa;AACzC,mBAAS,IAAI,GAAG,IAAI,eAAe,QAAQ,KAAK;AAC9C,kBAAM,MAAM,eAAe,IAAI,CAAC;AAChC,gBAAI,KAAK,WAAW,cAAc,EAAG,QAAO,IAAI,IAAI,MAAM,eAAe,MAAM,CAAC;AAAA,UAClF;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AACA,aAAO,CAAC,GAAG,MAAM;AAAA,IACnB;AAAA,EACF;AACF;AAGA,IAAM,aAA2B,4BAA4B;AAqB7D,IAAI,WAAyB,4BAA4B;AAoBlD,SAAS,cAAc,MAAiB,UAAyB;AACtE,WAAS,IAAI,SAAS,WAAW,QAAQ,GAAG,KAAK,UAAU,IAAI,CAAC;AAClE;AAEO,SAAS,aAAa,UAAqC;AAChE,QAAM,MAAM,SAAS,IAAI,SAAS,WAAW,QAAQ,CAAC;AACtD,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AAIF,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,gBAAgB,UAAyB;AACvD,WAAS,OAAO,SAAS,WAAW,QAAQ,CAAC;AAC/C;AAMO,SAAS,iBAAiB,UAAkB,UAAwB;AACzE,aAAW,IAAI,sBAAsB,UAAU,QAAQ;AACzD;AAEO,SAAS,gBAAgB,UAAiC;AAC/D,SAAO,WAAW,IAAI,sBAAsB,QAAQ;AACtD;AAEO,SAAS,mBAAmB,UAAwB;AACzD,aAAW,OAAO,sBAAsB,QAAQ;AAClD;AAMO,SAAS,eAAe,OAAe,UAAyB;AACrE,aAAW,IAAI,SAAS,WAAW,QAAQ,GAAG,KAAK;AACrD;AAEO,SAAS,cAAc,UAAkC;AAC9D,SAAO,WAAW,IAAI,SAAS,WAAW,QAAQ,CAAC;AACrD;AAEO,SAAS,iBAAiB,UAAyB;AACxD,aAAW,OAAO,SAAS,WAAW,QAAQ,CAAC;AACjD;AAMO,SAAS,eAAe,OAAe,UAAyB;AACrE,aAAW,IAAI,SAAS,WAAW,QAAQ,GAAG,KAAK;AACrD;AAEO,SAAS,cAAc,UAAkC;AAC9D,SAAO,WAAW,IAAI,SAAS,WAAW,QAAQ,CAAC;AACrD;AAEO,SAAS,iBAAiB,UAAyB;AACxD,aAAW,OAAO,SAAS,WAAW,QAAQ,CAAC;AACjD;AAMO,SAAS,gBAAgB,OAAe,UAAyB;AACtE,aAAW,IAAI,SAAS,kBAAkB,QAAQ,GAAG,KAAK;AAC5D;AAEO,SAAS,eAAe,UAAkC;AAC/D,SAAO,WAAW,IAAI,SAAS,kBAAkB,QAAQ,CAAC;AAC5D;AAEO,SAAS,kBAAkB,UAAyB;AACzD,aAAW,OAAO,SAAS,kBAAkB,QAAQ,CAAC;AACxD;AAKO,SAAS,cAAc,KAAa,UAAyB;AAClE,aAAW,IAAI,SAAS,gBAAgB,QAAQ,GAAG,GAAG;AACxD;AAEO,SAAS,aAAa,UAAkC;AAC7D,SAAO,WAAW,IAAI,SAAS,gBAAgB,QAAQ,CAAC;AAC1D;AAEO,SAAS,gBAAgB,UAAyB;AACvD,aAAW,OAAO,SAAS,gBAAgB,QAAQ,CAAC;AACtD;AAQO,SAAS,gBAAgB,OAAe,UAAyB;AACtE,aAAW,IAAI,SAAS,kBAAkB,QAAQ,GAAG,KAAK;AAC5D;AAEO,SAAS,eAAe,UAAkC;AAC/D,SAAO,WAAW,IAAI,SAAS,kBAAkB,QAAQ,CAAC;AAC5D;AAEO,SAAS,kBAAkB,UAAyB;AACzD,aAAW,OAAO,SAAS,kBAAkB,QAAQ,CAAC;AACxD;AAMO,SAAS,gBAAgB,UAAyB;AACvD,MAAI,UAAU;AACZ,oBAAgB,QAAQ;AACxB,qBAAiB,QAAQ;AACzB,qBAAiB,QAAQ;AACzB,oBAAgB,QAAQ;AACxB,sBAAkB,QAAQ;AAC1B,sBAAkB,QAAQ;AAC1B,uBAAmB,QAAQ;AAE3B,uBAAmB,GAAG,QAAQ,cAAc;AAC5C;AAAA,EACF;AAEA,kBAAgB;AAChB,mBAAiB;AACjB,mBAAiB;AACjB,kBAAgB;AAChB,oBAAkB;AAClB,oBAAkB;AAMlB,QAAM,SAAoB,CAAC;AAC3B,MAAI;AACF,QAAI,OAAO,mBAAmB,YAAa,QAAO,KAAK,cAAc;AAAA,EACvE,QAAQ;AAAA,EAER;AACA,MAAI;AACF,QAAI,OAAO,iBAAiB,YAAa,QAAO,KAAK,YAAY;AAAA,EACnE,QAAQ;AAAA,EAER;AAEA,QAAM,eAAe,oBAAI,IAAY;AACrC,aAAW,SAAS,QAAQ;AAC1B,UAAM,OAAiB,CAAC;AACxB,QAAI;AACF,eAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,cAAM,MAAM,MAAM,IAAI,CAAC;AACvB,YAAI,KAAK,WAAW,cAAc,EAAG,MAAK,KAAK,GAAG;AAAA,MACpD;AAAA,IACF,QAAQ;AACN;AAAA,IACF;AACA,eAAW,OAAO,MAAM;AACtB,mBAAa,IAAI,IAAI,MAAM,eAAe,MAAM,CAAC;AACjD,UAAI;AACF,cAAM,WAAW,GAAG;AAAA,MACtB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAIA,aAAW,SAAS,CAAC,YAAY,QAAQ,GAAG;AAC1C,QAAI;AACF,iBAAW,OAAO,MAAM,OAAO,KAAK,CAAC,EAAG,cAAa,IAAI,GAAG;AAAA,IAC9D,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,aAAW,OAAO,cAAc;AAC9B,eAAW,OAAO,GAAG;AACrB,aAAS,OAAO,GAAG;AAAA,EACrB;AACF;;;ACjbA,IAAM,0BAA0B;AAUzB,SAAS,mBAAmB,KAAa,eAAgC;AAC9E,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI,wBAAwB,KAAK,GAAG,EAAG,QAAO;AAC9C,MAAI,IAAI,WAAW,IAAI,EAAG,QAAO;AACjC,MAAI,IAAI,WAAW,GAAG,EAAG,QAAO;AAChC,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,QAAI,OAAO,aAAa,WAAW,OAAO,aAAa,SAAU,QAAO;AACxE,QAAI,OAAO,YAAY,OAAO,SAAU,QAAO;AAC/C,WAAO,OAAO,WAAW;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,sBAAsB,GAAW,GAAoB;AACnE,QAAM,KAAK,IAAI,YAAY,EAAE,OAAO,CAAC;AACrC,QAAM,KAAK,IAAI,YAAY,EAAE,OAAO,CAAC;AACrC,MAAI,GAAG,WAAW,KAAK,GAAG,WAAW,EAAG,QAAO,GAAG,WAAW,GAAG;AAChE,QAAM,MAAM,KAAK,IAAI,GAAG,QAAQ,GAAG,MAAM;AACzC,MAAI,OAAO,GAAG,SAAS,GAAG;AAC1B,WAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,YAAQ,GAAG,IAAI,GAAG,MAAM,IAAI,GAAG,IAAI,GAAG,MAAM;AAAA,EAC9C;AACA,SAAO,SAAS;AAClB;;;ACrCA,IAAM,UACJ,OAAO,WAAW,eAAgB,OAAkB,SAAU,SAAoB;AAEpF,SAAS,YAA2B;AAClC,SAAQ,WAA6B;AACvC;AAyCA,SAAS,gBAAgB,OAA2B;AAClD,QAAM,SAAS,MAAM,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AACzD,QAAM,SAAS,OAAO,OAAO,OAAO,UAAW,IAAK,OAAO,SAAS,KAAM,GAAI,GAAG;AACjF,QAAM,SAAS,KAAK,MAAM;AAC1B,QAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;AAC1C,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,UAAM,CAAC,IAAI,OAAO,WAAW,CAAC;AAAA,EAChC;AACA,SAAO;AACT;AAGO,SAAS,iBAAiB,OAA+C;AAC9E,MAAI;AACF,UAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,UAAM,OAAO,IAAI,YAAY,EAAE,OAAO,gBAAgB,MAAM,CAAC,CAAC,CAAC;AAC/D,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,gBAAgB,OAA+C;AAC7E,MAAI;AACF,UAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,UAAM,OAAO,IAAI,YAAY,EAAE,OAAO,gBAAgB,MAAM,CAAC,CAAC,CAAC;AAC/D,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,gBAAgB,KAAqB;AAC5C,SAAO,IAAI,QAAQ,QAAQ,EAAE;AAC/B;AAQA,IAAI,aAAwE;AAC5E,IAAI,kBAAwF;AAC5F,IAAM,oBAAoB,IAAI,KAAK;AAEnC,IAAM,mBAAmB;AAQzB,eAAsB,kBAAkB,SAAmD;AACzF,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,mBAAmB,gBAAgB,YAAY,WAAW,MAAM,gBAAgB,YAAY,mBAAmB;AACjH,WAAO,gBAAgB;AAAA,EACzB;AAEA,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,GAAG,OAAO,+CAA+C;AAAA,MAC/E,QAAQ,YAAY,QAAQ,gBAAgB;AAAA,IAC9C,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,MAAO,MAAM,IAAI,KAAK;AAC5B,sBAAkB,EAAE,SAAS,KAAK,WAAW,IAAI;AACjD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWA,eAAsB,UAAU,SAAiB,UAA4B,CAAC,GAAyB;AACrG,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,CAAC,QAAQ,gBAAgB,cAAc,WAAW,YAAY,WAAW,MAAM,WAAW,YAAY,mBAAmB;AAC3H,WAAO,WAAW;AAAA,EACpB;AAEA,QAAM,YAAY,MAAM,kBAAkB,OAAO;AACjD,QAAM,UAAU,WAAW,YAAY,GAAG,OAAO;AAEjD,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,SAAS;AAAA,MAC/B,QAAQ,YAAY,QAAQ,gBAAgB;AAAA,MAC5C,GAAI,QAAQ,eAAe,EAAE,OAAO,WAA2B,IAAI,CAAC;AAAA,IACtE,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,iBAAa,EAAE,SAAS,MAAM,WAAW,IAAI;AAC7C,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASA,eAAsB,qBAAqB,OAAe,KAAgC;AACxF,MAAI;AACF,UAAM,CAAC,WAAW,YAAY,SAAS,IAAI,MAAM,MAAM,GAAG;AAC1D,QAAI,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,IAAI,EAAG,QAAO;AAE3C,UAAM,IAAI,UAAU;AACpB,QAAI,CAAC,EAAG,QAAO;AAEf,UAAM,YAAY,MAAM,EAAE,OAAO;AAAA,MAC/B;AAAA,MACA,EAAE,KAAK,OAAO,GAAG,IAAI,GAAG,GAAG,IAAI,GAAG,KAAK,SAAS,KAAK,MAAM;AAAA,MAC3D,EAAE,MAAM,qBAAqB,MAAM,UAAU;AAAA,MAC7C;AAAA,MACA,CAAC,QAAQ;AAAA,IACX;AAEA,UAAM,iBAAiB,gBAAgB,SAAS;AAChD,UAAM,OAAO,IAAI,YAAY,EAAE,OAAO,GAAG,SAAS,IAAI,UAAU,EAAE;AAClE,WAAO,MAAM,EAAE,OAAO;AAAA,MACpB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,cAAc,aAAsC;AACxE,QAAM,IAAI,UAAU;AACpB,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,OAAO,MAAM,EAAE,OAAO,OAAO,WAAW,IAAI,YAAY,EAAE,OAAO,WAAW,CAAC;AACnF,QAAM,QAAQ,IAAI,WAAW,IAAI;AACjC,QAAM,OAAO,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC;AAC5C,MAAI,SAAS;AACb,aAAW,KAAK,MAAM;AACpB,cAAU,OAAO,aAAa,CAAC;AAAA,EACjC;AACA,SAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC/E;AAGA,eAAsB,gBACpB,SACA,aACA,gBACA,kBACA,UAAkC,CAAC,GACH;AAChC,QAAM,EAAE,gCAAgC,MAAM,cAAc,IAAI;AAEhE,QAAM,SAAS,gBAAgB,OAAO;AACtC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,SAAS,oBAAoB,mCAAe;AAAA,EACxD;AAEA,QAAM,MAAM,OAAO;AACnB,MAAI,OAAO,QAAQ,YAAa,QAAQ,WAAW,QAAQ,SAAU;AACnE,UAAM,IAAI,SAAS,4BAA4B,+DAAuB,GAAG,EAAE;AAAA,EAC7E;AAEA,QAAM,UAAU,gBAAgB,cAAc;AAE9C,QAAM,YAAY,MAAM,kBAAkB,OAAO;AACjD,QAAM,mBAAmB,gBAAgB,WAAW,UAAU,OAAO;AAGrE,MAAI,QAAQ,SAAS;AACnB,UAAM,OAAO,MAAM,UAAU,OAAO;AACpC,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,SAAS,8BAA8B,kEAA0B;AAAA,IAC7E;AACA,UAAM,MAAM,OAAO,OAAO,QAAQ,WAAW,OAAO,MAAM;AAE1D,UAAM,kBAAkB,CAAC,QACvB,IAAI,KAAK;AAAA,MACP,CAAC,MACC,EAAE,QAAQ,SACV,EAAE,QAAQ,WACV,EAAE,QAAQ,UACT,MAAM,EAAE,QAAQ,MAAM;AAAA,IAC3B;AACF,UAAM,YAAY,OAAO,SAAsC;AAC7D,iBAAW,OAAO,MAAM;AACtB,YAAI,MAAM,qBAAqB,SAAS,GAAG,EAAG,QAAO;AAAA,MACvD;AACA,aAAO;AAAA,IACT;AAEA,QAAI,aAAa,gBAAgB,IAAI;AACrC,QAAI,WAAW,WAAW,SAAS,IAAI,MAAM,UAAU,UAAU,IAAI;AAIrE,QAAI,WAAW,WAAW,KAAK,CAAC,UAAU;AACxC,YAAM,YAAY,MAAM,UAAU,SAAS,EAAE,cAAc,KAAK,CAAC;AACjE,UAAI,WAAW;AACb,qBAAa,gBAAgB,SAAS;AACtC,mBAAW,WAAW,SAAS,IAAI,MAAM,UAAU,UAAU,IAAI;AAAA,MACnE;AAAA,IACF;AAEA,QAAI,WAAW,WAAW,GAAG;AAC3B,YAAM,IAAI,SAAS,8BAA8B,oEAAuB;AAAA,IAC1E;AACA,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,SAAS,8BAA8B,+CAAiB;AAAA,IACpE;AAAA,EACF;AAIA,MAAI,QAAQ,SAAS;AACnB,QAAI,+BAA+B;AACjC,YAAM,OAAO,MAAM,UAAU,OAAO;AACpC,YAAM,WAAW,MAAM,MAAM,KAAK,CAAC,MAAM,EAAE,QAAQ,WAAW,EAAE,QAAQ,KAAK;AAC7E,UAAI,UAAU;AACZ,cAAM,IAAI,SAAS,4BAA4B,2EAAmC;AAAA,MACpF;AAAA,IACF;AACA,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IAEF;AAAA,EACF;AAEA,QAAM,UAAU,iBAAiB,OAAO;AACxC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,SAAS,oBAAoB,2CAAuB;AAAA,EAChE;AAEA,QAAM,cAAc,OAAO,QAAQ,QAAQ,WAAW,gBAAgB,QAAQ,GAAG,IAAI;AACrF,MAAI,gBAAgB,kBAAkB;AACpC,UAAM,IAAI,SAAS,4BAA4B,oCAAqB;AAAA,EACtE;AAGA,QAAM,MAAM,QAAQ;AACpB,QAAM,UAAU,MAAM,QAAQ,GAAG,IAAI,MAAM,OAAO,QAAQ,WAAW,CAAC,GAAG,IAAI,CAAC;AAC9E,MAAI,CAAC,QAAQ,SAAS,gBAAgB,GAAG;AACvC,UAAM,IAAI,SAAS,8BAA8B,sCAAuB;AAAA,EAC1E;AACA,MAAI,QAAQ,SAAS,KAAK,QAAQ,QAAQ,kBAAkB;AAC1D,UAAM,IAAI,SAAS,8BAA8B,2EAAwC;AAAA,EAC3F;AAGA,MAAI,OAAO,QAAQ,QAAQ,UAAU;AACnC,UAAM,IAAI,SAAS,oBAAoB,wCAAoB;AAAA,EAC7D;AACA,MAAI,KAAK,IAAI,KAAK,QAAQ,MAAM,MAAO,KAAQ;AAC7C,UAAM,IAAI,SAAS,oBAAoB,6BAAc;AAAA,EACvD;AAGA,MAAI,OAAO,QAAQ,QAAQ,UAAU;AACnC,UAAM,IAAI,SAAS,oBAAoB,wCAAoB;AAAA,EAC7D;AACA,MAAI,QAAQ,MAAM,MAAO,KAAK,IAAI,IAAI,KAAQ;AAC5C,UAAM,IAAI,SAAS,oBAAoB,6FAA4B;AAAA,EACrE;AAEA,MAAI,OAAO,QAAQ,QAAQ,YAAY,CAAC,QAAQ,KAAK;AACnD,UAAM,IAAI,SAAS,wBAAwB,2BAAiB;AAAA,EAC9D;AAGA,MAAI,OAAO,QAAQ,YAAY,YAAY,QAAQ,SAAS;AAC1D,UAAM,SAAS,MAAM,cAAc,WAAW;AAC9C,QAAI,CAAC,sBAAsB,QAAQ,QAAQ,OAAO,GAAG;AACnD,YAAM,IAAI,SAAS,6BAA6B,qCAAsB;AAAA,IACxE;AAAA,EACF;AAQA,MAAI,kBAAkB,QAAW;AAC/B,UAAM,aAAa,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AACvE,QAAI,CAAC,cAAc,CAAC,sBAAsB,eAAe,UAAU,GAAG;AACpE,YAAM,IAAI,SAAS,2BAA2B,mCAAoB;AAAA,IACpE;AAAA,EACF,WAAW,OAAO,QAAQ,UAAU,YAAY,QAAQ,OAAO;AAC7D,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,KAAK,QAAQ,IAAI;AAC5B;;;AC1WO,IAAM,qBAAqB;AAGlC,eAAsB,iBACpB,OACA,OAAoB,CAAC,GACrB,YAAoB,oBACD;AACnB,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAC5D,MAAI;AACF,WAAO,MAAM,MAAM,OAAO,EAAE,GAAG,MAAM,QAAQ,WAAW,OAAO,CAAC;AAAA,EAClE,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;;;AC6BA,IAAM,oBAAoB;AAK1B,IAAM,iBAAiB;AAgBvB,SAAS,yBACP,MACA,qBACM;AACN,MAAI,OAAO,KAAK,iBAAiB,YAAY,CAAC,KAAK,cAAc;AAC/D,UAAM,IAAI,SAAS,wBAAwB,6CAAyB;AAAA,EACtE;AACA,MACE,OAAO,KAAK,eAAe,YAC3B,CAAC,OAAO,SAAS,KAAK,UAAU,KAChC,KAAK,cAAc,GACnB;AACA,UAAM,IAAI;AAAA,MACR;AAAA,MACA,+CAA2B,OAAO,KAAK,UAAU,CAAC;AAAA,IACpD;AAAA,EACF;AACA,MACE,wBACC,OAAO,KAAK,kBAAkB,YAAY,CAAC,KAAK,gBACjD;AACA,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAyFA,IAAM,sBAAsB,oBAAI,IAAI;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAoCM,IAAM,aAAN,MAAM,WAAU;AAAA,EAgBrB,YAAY,QAAyB;AAZrC,SAAQ,aAAmC;AAC3C,SAAQ,sBAA8B;AACtC,SAAQ,eAA0C;AAElD;AAAA,SAAQ,yBAAyB;AAS/B,QAAI,CAAC,OAAO,SAAU,OAAM,IAAI,SAAS,kBAAkB,mCAAe;AAC1E,QAAI,CAAC,OAAO,YAAa,OAAM,IAAI,SAAS,kBAAkB,sCAAkB;AAChF,QAAI,CAAC,OAAO,WAAY,OAAM,IAAI,SAAS,kBAAkB,qCAAiB;AAG9E,UAAM,OAAO,OAAO,WAAW,QAAQ,QAAQ,EAAE;AACjD,SAAK,SAAS,EAAE,GAAG,QAAQ,YAAY,KAAK;AAC5C,SAAK,eAAe,OAAO,iBAAiB,MAAM,QAAQ,QAAQ,EAAE;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,YAAY,MAAuB;AACjC,QAAI,KAAK,OAAO,OAAO;AACrB,cAAQ,KAAK,aAAa,GAAG,IAAI;AAAA,IACnC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcQ,mBAAmB,GAAwC;AACjE,QAAI,KAAK,OAAO,mCAAoC,QAAO;AAC3D,UAAM,iBAAiB,oBAAI,IAAY;AACvC,QAAI;AACF,qBAAe,IAAI,IAAI,IAAI,KAAK,OAAO,UAAU,EAAE,MAAM;AACzD,qBAAe,IAAI,IAAI,IAAI,KAAK,WAAW,EAAE,MAAM;AAAA,IACrD,QAAQ;AACN,aAAO;AAAA,IACT;AACA,UAAM,aAAa,CAAC,aAA8B;AAChD,UAAI;AACF,eAAO,eAAe,IAAI,IAAI,IAAI,QAAQ,EAAE,MAAM;AAAA,MACpD,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAEA,UAAM,eAAe,IAAI,IAAI,KAAK,OAAO,UAAU,EAAE;AACrD,QAAI,kBAAkB;AACtB,QAAI;AACF,wBAAkB,IAAI,IAAI,EAAE,sBAAsB,EAAE,WAAW;AAAA,IACjE,QAAQ;AACN,wBAAkB;AAAA,IACpB;AACA,UAAM,MAAgB,CAAC;AACvB,QAAI,CAAC,gBAAiB,KAAI,KAAK,wBAAwB;AACvD,QAAI,CAAC,WAAW,EAAE,cAAc,EAAG,KAAI,KAAK,gBAAgB;AAC5D,QAAI,CAAC,WAAW,EAAE,iBAAiB,EAAG,KAAI,KAAK,mBAAmB;AAClE,QAAI,IAAI,SAAS,GAAG;AAClB,UAAI,CAAC,KAAK,wBAAwB;AAChC,aAAK,yBAAyB;AAC9B,gBAAQ;AAAA,UACN,wGAAqE,IAAI,KAAK,IAAI,CAAC;AAAA,QAErF;AAAA,MACF;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,gBAA+C;AAC3D,UAAM,MAAM,KAAK,IAAI;AAGrB,QACE,KAAK,cACL,MAAM,KAAK,sBAAsB,WAAU,kBAC3C;AACA,aAAO,KAAK;AAAA,IACd;AAIA,UAAM,MAAM,GAAG,KAAK,WAAW;AAE/B,QAAI;AACF,YAAM,aAAa,IAAI,gBAAgB;AACvC,YAAM,YAAY;AAAA,QAChB,MAAM,WAAW,MAAM;AAAA,QACvB,WAAU;AAAA,MACZ;AAEA,YAAM,MAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,WAAW,OAAO,CAAC;AAC1D,mBAAa,SAAS;AAEtB,UAAI,CAAC,IAAI,IAAI;AACX,gBAAQ;AAAA,UACN,2DAAuC,IAAI,MAAM;AAAA,QACnD;AACA,eAAO;AAAA,MACT;AAEA,YAAM,MAAO,MAAM,IAAI,KAAK;AAE5B,YAAM,YAAY,KAAK,mBAAmB,GAAG;AAC7C,UAAI,CAAC,UAAW,QAAO;AACvB,WAAK,aAAa;AAClB,WAAK,sBAAsB;AAC3B,aAAO,KAAK;AAAA,IACd,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,sDAAkC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MACpF;AAEA,aAAO,KAAK,cAAc;AAAA,IAC5B;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,wBAAyC;AACrD,UAAM,IAAI,MAAM,KAAK,cAAc;AACnC,QAAI,EAAG,QAAO,EAAE;AAChB,WAAO,GAAG,KAAK,OAAO,UAAU;AAAA,EAClC;AAAA;AAAA,EAGA,MAAc,oBAAqC;AACjD,QAAI,KAAK,OAAO,cAAe,QAAO,GAAG,KAAK,WAAW;AACzD,UAAM,IAAI,MAAM,KAAK,cAAc;AACnC,QAAI,EAAG,QAAO,EAAE;AAChB,WAAO,GAAG,KAAK,OAAO,UAAU;AAAA,EAClC;AAAA;AAAA,EAGA,MAAc,uBAAwC;AACpD,QAAI,KAAK,OAAO,cAAe,QAAO,GAAG,KAAK,WAAW;AACzD,UAAM,IAAI,MAAM,KAAK,cAAc;AACnC,QAAI,EAAG,QAAO,EAAE;AAChB,WAAO,GAAG,KAAK,OAAO,UAAU;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,wBAAwB,WAAyB;AACvD,QAAI,mBAAmB,WAAW,OAAO,SAAS,MAAM,GAAG;AAEzD,oBAAc,WAAW,KAAK,OAAO,QAAQ;AAAA,IAC/C,OAAO;AACL,cAAQ,KAAK,2FAAoC,SAAS,EAAE;AAAA,IAC9D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBA,MAAM,MAAM,WAAoB,UAAwB,CAAC,GAAkB;AACzE,UAAM,WAAW,qBAAqB;AACtC,UAAM,YAAY,MAAM,sBAAsB,QAAQ;AACtD,UAAM,QAAQ,cAAc;AAE5B,UAAM,QAAQ,cAAc;AAG5B,qBAAiB,KAAK,OAAO,UAAU,QAAQ;AAC/C,mBAAe,OAAO,KAAK,OAAO,QAAQ;AAC1C,mBAAe,OAAO,KAAK,OAAO,QAAQ;AAE1C,QAAI,QAAQ,WAAW,QAAQ;AAC7B,sBAAgB,OAAO,KAAK,OAAO,QAAQ;AAAA,IAC7C,OAAO;AACL,wBAAkB,KAAK,OAAO,QAAQ;AAAA,IACxC;AAEA,QAAI,WAAW;AACb,WAAK,wBAAwB,SAAS;AAAA,IACxC;AAGA,UAAM,oBAAoB,MAAM,KAAK,sBAAsB;AAC3D,UAAM,SAAS,IAAI,gBAAgB;AACnC,WAAO,IAAI,iBAAiB,MAAM;AAClC,WAAO,IAAI,aAAa,KAAK,OAAO,QAAQ;AAC5C,WAAO,IAAI,gBAAgB,KAAK,OAAO,WAAW;AAClD,WAAO,IAAI,SAAS,KAAK,OAAO,UAAU,gBAAgB;AAC1D,WAAO,IAAI,SAAS,KAAK;AACzB,WAAO,IAAI,SAAS,KAAK;AACzB,WAAO,IAAI,kBAAkB,SAAS;AACtC,WAAO,IAAI,yBAAyB,MAAM;AAC1C,QAAI,QAAQ,QAAQ;AAClB,aAAO,IAAI,UAAU,QAAQ,MAAM;AAAA,IACrC;AAGA,WAAO,SAAS,OAAO,GAAG,iBAAiB,IAAI,OAAO,SAAS,CAAC;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,YAAY,WAAoB,UAAwB,CAAC,GAAoB;AACjF,UAAM,WAAW,qBAAqB;AACtC,UAAM,YAAY,MAAM,sBAAsB,QAAQ;AACtD,UAAM,QAAQ,cAAc;AAE5B,UAAM,QAAQ,cAAc;AAE5B,qBAAiB,KAAK,OAAO,UAAU,QAAQ;AAC/C,mBAAe,OAAO,KAAK,OAAO,QAAQ;AAC1C,mBAAe,OAAO,KAAK,OAAO,QAAQ;AAE1C,QAAI,QAAQ,WAAW,QAAQ;AAC7B,sBAAgB,OAAO,KAAK,OAAO,QAAQ;AAAA,IAC7C,OAAO;AACL,wBAAkB,KAAK,OAAO,QAAQ;AAAA,IACxC;AAEA,QAAI,UAAW,MAAK,wBAAwB,SAAS;AAErD,UAAM,oBAAoB,MAAM,KAAK,sBAAsB;AAC3D,UAAM,SAAS,IAAI,gBAAgB;AACnC,WAAO,IAAI,iBAAiB,MAAM;AAClC,WAAO,IAAI,aAAa,KAAK,OAAO,QAAQ;AAC5C,WAAO,IAAI,gBAAgB,KAAK,OAAO,WAAW;AAClD,WAAO,IAAI,SAAS,KAAK,OAAO,UAAU,gBAAgB;AAC1D,WAAO,IAAI,SAAS,KAAK;AACzB,WAAO,IAAI,SAAS,KAAK;AACzB,WAAO,IAAI,kBAAkB,SAAS;AACtC,WAAO,IAAI,yBAAyB,MAAM;AAC1C,QAAI,QAAQ,QAAQ;AAClB,aAAO,IAAI,UAAU,QAAQ,MAAM;AAAA,IACrC;AAEA,WAAO,GAAG,iBAAiB,IAAI,OAAO,SAAS,CAAC;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmCA,MAAM,WACJ,UAAmE,CAAC,GAChD;AACpB,UAAM,WAAW,qBAAqB;AACtC,UAAM,YAAY,MAAM,sBAAsB,QAAQ;AACtD,UAAM,QAAQ,cAAc;AAC5B,UAAM,QAAQ,cAAc;AAC5B,UAAM,aAAa,cAAc;AAEjC,qBAAiB,KAAK,OAAO,UAAU,QAAQ;AAC/C,mBAAe,OAAO,KAAK,OAAO,QAAQ;AAC1C,mBAAe,OAAO,KAAK,OAAO,QAAQ;AAE1C,qBAAiB,GAAG,KAAK,OAAO,QAAQ,gBAAgB,UAAU;AAElE,QAAI,QAAQ,WAAW;AACrB,WAAK,wBAAwB,QAAQ,SAAS;AAAA,IAChD;AAEA,UAAM,oBAAoB,MAAM,KAAK,sBAAsB;AAC3D,UAAM,SAAS,IAAI,gBAAgB;AACnC,WAAO,IAAI,iBAAiB,MAAM;AAClC,WAAO,IAAI,aAAa,KAAK,OAAO,QAAQ;AAC5C,WAAO,IAAI,gBAAgB,KAAK,OAAO,WAAW;AAClD,WAAO,IAAI,SAAS,KAAK,OAAO,UAAU,gBAAgB;AAC1D,WAAO,IAAI,SAAS,KAAK;AACzB,WAAO,IAAI,SAAS,KAAK;AACzB,WAAO,IAAI,kBAAkB,SAAS;AACtC,WAAO,IAAI,yBAAyB,MAAM;AAE1C,WAAO,IAAI,eAAe,UAAU;AAEpC,UAAM,QAAQ,QAAQ,SAAS;AAC/B,UAAM,SAAS,QAAQ,UAAU;AACjC,UAAM,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,QAAQ,SAAS,CAAC;AAC1D,UAAM,MAAM,KAAK,IAAI,IAAI,OAAO,OAAO,SAAS,UAAU,CAAC;AAE3D,UAAM,gBAAgB;AAAA,MACpB,SAAS,KAAK;AAAA,MACd,UAAU,MAAM;AAAA,MAChB,QAAQ,KAAK,MAAM,IAAI,CAAC;AAAA,MACxB,OAAO,KAAK,MAAM,GAAG,CAAC;AAAA,MACtB;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,GAAG;AAEV,UAAM,QAAQ,OAAO;AAAA,MACnB,GAAG,iBAAiB,IAAI,OAAO,SAAS,CAAC;AAAA,MACzC;AAAA,MACA;AAAA,IACF;AAEA,QAAI,CAAC,OAAO;AACV,yBAAmB,KAAK,OAAO,QAAQ;AACvC,uBAAiB,KAAK,OAAO,QAAQ;AACrC,uBAAiB,KAAK,OAAO,QAAQ;AACrC,yBAAmB,GAAG,KAAK,OAAO,QAAQ,cAAc;AACxD,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAGA,UAAM,oBAAoB,IAAI,IAAI,KAAK,OAAO,WAAW,EAAE;AAE3D,QAAI;AAEF,YAAM,MAAM;AAEZ,aAAO,MAAM,IAAI,QAAmB,CAAC,SAAS,WAAW;AACvD,YAAI,YAAY;AAEhB,cAAM,gBAAgB,CAAC,UAAwB;AAC7C,cAAI,UAAW;AAEf,cAAI,MAAM,WAAW,kBAAmB;AACxC,cAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,SAAS,6BAA8B;AACrE,cAAI,CAAC,MAAM,KAAK,YAAa;AAG7B,gBAAM,aAAa,gBAAgB,GAAG,KAAK,OAAO,QAAQ,cAAc;AACxE,cAAI,CAAC,cAAc,CAAC,sBAAsB,MAAM,KAAK,SAAS,IAAI,UAAU,EAAG;AAC/E,6BAAmB,GAAG,KAAK,OAAO,QAAQ,cAAc;AAIxD,cAAI;AACF,YAAC,MAAM,QAA0B;AAAA,cAC/B,EAAE,MAAM,yBAAyB,OAAO,WAAW;AAAA,cACnD,MAAM;AAAA,YACR;AAAA,UACF,QAAQ;AAAA,UAER;AAEA,sBAAY;AACZ,kBAAQ;AAER,cAAI,SAAS,CAAC,MAAM,QAAQ;AAC1B,kBAAM,MAAM;AAAA,UACd;AAEA,eAAK,eAAe,MAAM,KAAK,WAAW,EACvC,KAAK,CAAC,cAAc;AAGnB,gBAAI,cAAc,MAAM;AACtB;AAAA,gBACE,IAAI,SAAS,qBAAqB,0FAAoB;AAAA,cACxD;AACA;AAAA,YACF;AACA,oBAAQ,SAAS;AAAA,UACnB,CAAC,EACA,MAAM,MAAM;AAAA,QACjB;AAEA,cAAM,YAAY,YAAY,MAAM;AAClC,cAAI,MAAM,QAAQ;AAChB,gBAAI,CAAC,WAAW;AACd,0BAAY;AACZ,sBAAQ;AACR;AAAA,gBACE,IAAI;AAAA,kBACF;AAAA,kBACA;AAAA,gBACF;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,QACF,GAAG,GAAG;AAEN,cAAM,UAAU,MAAM;AACpB,wBAAc,SAAS;AACvB,iBAAO,oBAAoB,WAAW,aAAa;AACnD,6BAAmB,GAAG,KAAK,OAAO,QAAQ,cAAc;AAAA,QAC1D;AAEA,eAAO,iBAAiB,WAAW,aAAa;AAAA,MAClD,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,yBAAmB,KAAK,OAAO,QAAQ;AACvC,uBAAiB,KAAK,OAAO,QAAQ;AACrC,uBAAiB,KAAK,OAAO,QAAQ;AACrC,yBAAmB,GAAG,KAAK,OAAO,QAAQ,cAAc;AACxD,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,eACJ,aAC2B;AAC3B,UAAM,MAAM,IAAI,IAAI,WAAW;AAC/B,UAAM,SAAS,IAAI;AAGnB,UAAM,QAAQ,OAAO,IAAI,OAAO;AAChC,QAAI,OAAO;AAIT,YAAM,qBAAqB,OAAO,IAAI,OAAO;AAC7C,YAAM,aAAa,eAAe,KAAK,OAAO,QAAQ;AACtD,YAAM,qBAAqB,cAAc,KAAK,OAAO,QAAQ;AAC7D,UACE,oBAAoB,IAAI,KAAK,KAC7B,cACA,sBACA,sBAAsB,oBAAoB,sBAAsB,EAAE,KAClE,sBAAsB,YAAY,sBAAsB,EAAE,GAC1D;AAEA,yBAAiB,KAAK,OAAO,QAAQ;AACrC,yBAAiB,KAAK,OAAO,QAAQ;AACrC,2BAAmB,KAAK,OAAO,QAAQ;AACvC,0BAAkB,KAAK,OAAO,QAAQ;AACtC,eAAO;AAAA,MACT;AAEA,uBAAiB,KAAK,OAAO,QAAQ;AACrC,uBAAiB,KAAK,OAAO,QAAQ;AACrC,yBAAmB,KAAK,OAAO,QAAQ;AACvC,wBAAkB,KAAK,OAAO,QAAQ;AACtC,YAAM,OAAO,OAAO,IAAI,mBAAmB,KAAK;AAChD,YAAM,IAAI,SAAS,uBAAuB,KAAK,GAAG,6BAAS,IAAI,EAAE;AAAA,IACnE;AAEA,UAAM,OAAO,OAAO,IAAI,MAAM;AAC9B,UAAM,gBAAgB,OAAO,IAAI,OAAO;AAExC,QAAI,CAAC,MAAM;AAET,uBAAiB,KAAK,OAAO,QAAQ;AACrC,uBAAiB,KAAK,OAAO,QAAQ;AACrC,yBAAmB,KAAK,OAAO,QAAQ;AACvC,wBAAkB,KAAK,OAAO,QAAQ;AACtC,YAAM,IAAI,SAAS,wBAAwB,wDAA+B;AAAA,IAC5E;AAGA,UAAM,aAAa,cAAc,KAAK,OAAO,QAAQ;AACrD,QAAI,CAAC,cAAc,CAAC,sBAAsB,YAAY,iBAAiB,EAAE,GAAG;AAE1E,uBAAiB,KAAK,OAAO,QAAQ;AACrC,uBAAiB,KAAK,OAAO,QAAQ;AACrC,yBAAmB,KAAK,OAAO,QAAQ;AACvC,wBAAkB,KAAK,OAAO,QAAQ;AACtC,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAIA,UAAM,gBAAgB,cAAc,KAAK,OAAO,QAAQ,KAAK;AAG7D,UAAM,WAAW,gBAAgB,KAAK,OAAO,QAAQ;AACrD,QAAI,CAAC,UAAU;AACb,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAGA,UAAM,gBAAgB,MAAM,KAAK,kBAAkB;AACnD,UAAM,OAAO,IAAI,gBAAgB;AACjC,SAAK,IAAI,cAAc,oBAAoB;AAC3C,SAAK,IAAI,QAAQ,IAAI;AACrB,SAAK,IAAI,iBAAiB,QAAQ;AAClC,SAAK,IAAI,aAAa,KAAK,OAAO,QAAQ;AAC1C,SAAK,IAAI,gBAAgB,KAAK,OAAO,WAAW;AAChD,QAAI,KAAK,OAAO,cAAc;AAC5B,WAAK,IAAI,iBAAiB,KAAK,OAAO,YAAY;AAAA,IACpD;AAEA,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,iBAAiB,eAAe;AAAA,QAC1C,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,QAC/D,MAAM,KAAK,SAAS;AAAA,MACtB,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,YAAM,IAAI,SAAS,iBAAiB,wCAAU,GAAG;AAAA,IACnD;AAEA,QAAI,CAAC,IAAI,IAAI;AAGX,uBAAiB,KAAK,OAAO,QAAQ;AACrC,uBAAiB,KAAK,OAAO,QAAQ;AACrC,yBAAmB,KAAK,OAAO,QAAQ;AACvC,wBAAkB,KAAK,OAAO,QAAQ;AACtC,UAAI,UAAmC,CAAC;AACxC,UAAI;AAAE,kBAAU,MAAM,IAAI,KAAK;AAAA,MAAG,QAAQ;AAAA,MAAe;AACzD,YAAM,cAAe,QAAQ,SAAoB;AACjD,YAAM,IAAI;AAAA,QACR,uBAAuB,WAAW;AAAA,QACjC,QAAQ,qBAAgC,wCAAoB,IAAI,MAAM;AAAA,MACzE;AAAA,IACF;AAIA,UAAM,OAAsB,MAAM,IAAI,KAAK;AAI3C,6BAAyB,MAAM,IAAI;AAInC,UAAM,mBAAmB,KAAK,OAAO,UAAU,kBAAkB,MAAM,GAAG,EAAE,OAAO,OAAO;AAC1F,QAAI,gBAAgB,SAAS,QAAQ,KAAK,CAAC,KAAK,UAAU;AAExD,uBAAiB,KAAK,OAAO,QAAQ;AACrC,uBAAiB,KAAK,OAAO,QAAQ;AACrC,yBAAmB,KAAK,OAAO,QAAQ;AACvC,wBAAkB,KAAK,OAAO,QAAQ;AACtC,sBAAgB,KAAK,OAAO,QAAQ;AACpC,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAGA,QAAI,KAAK,UAAU;AACjB,UAAI;AACF,cAAM;AAAA,UACJ,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK,OAAO;AAAA,UACZ,KAAK,OAAO;AAAA,UACZ,EAAE,cAAc;AAAA,QAClB;AAAA,MACF,SAAS,KAAK;AAEZ,wBAAgB,KAAK,OAAO,QAAQ;AACpC,cAAM;AAAA,MACR;AAAA,IACF;AAKA,qBAAiB,KAAK,OAAO,QAAQ;AACrC,qBAAiB,KAAK,OAAO,QAAQ;AACrC,uBAAmB,KAAK,OAAO,QAAQ;AACvC,sBAAkB,KAAK,OAAO,QAAQ;AAEtC,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,YAAuB;AAAA,MAC3B,cAAc,KAAK;AAAA,MACnB,YAAY,KAAK;AAAA,MACjB,YAAY,KAAK;AAAA,MACjB,eAAe,KAAK;AAAA,MACpB,UAAU,KAAK;AAAA,MACf,WAAW;AAAA,MACX,YAAY,MAAM,KAAK,aAAa;AAAA,IACtC;AAEA,kBAAc,WAAW,KAAK,OAAO,QAAQ;AAC7C,SAAK,SAAS,kDAA8B,EAAE,UAAU,KAAK,OAAO,SAAS,CAAC;AAC9E,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,eAAmC;AAEvC,QAAI,KAAK,aAAc,QAAO,KAAK;AAEnC,SAAK,eAAe,KAAK,gBAAgB;AACzC,QAAI;AACF,aAAO,MAAM,KAAK;AAAA,IACpB,UAAE;AACA,WAAK,eAAe;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAc,kBAAsC;AAClD,UAAM,UAAU,aAAa,KAAK,OAAO,QAAQ;AACjD,QAAI,CAAC,SAAS,eAAe;AAC3B,YAAM,IAAI,SAAS,oBAAoB,8CAAqB;AAAA,IAC9D;AAEA,UAAM,gBAAgB,MAAM,KAAK,kBAAkB;AACnD,UAAM,OAAO,IAAI,gBAAgB;AACjC,SAAK,IAAI,cAAc,eAAe;AACtC,SAAK,IAAI,iBAAiB,QAAQ,aAAa;AAC/C,SAAK,IAAI,aAAa,KAAK,OAAO,QAAQ;AAC1C,QAAI,KAAK,OAAO,cAAc;AAC5B,WAAK,IAAI,iBAAiB,KAAK,OAAO,YAAY;AAAA,IACpD;AAEA,QAAI,MAAuB;AAC3B,QAAI;AACJ,aAAS,UAAU,GAAG,UAAU,GAAG,WAAW;AAC5C,UAAI;AACF,cAAM,MAAM,iBAAiB,eAAe;AAAA,UAC1C,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,UAC/D,MAAM,KAAK,SAAS;AAAA,QACtB,CAAC;AACD,kBAAU;AACV;AAAA,MACF,SAAS,KAAK;AACZ,kBAAU;AACV,YAAI,UAAU,EAAG,OAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,GAAI,CAAC;AAAA,MAC/D;AAAA,IACF;AACA,QAAI,WAAW,CAAC,KAAK;AACnB,YAAM,IAAI,SAAS,iBAAiB,2DAAmB,OAAO;AAAA,IAChE;AAEA,QAAI,CAAC,IAAI,IAAI;AACX,UAAI,UAAmC,CAAC;AACxC,UAAI;AAAE,kBAAU,MAAM,IAAI,KAAK;AAAA,MAAG,QAAQ;AAAA,MAAe;AACzD,YAAM,YAAa,QAAQ,SAAoB;AAI/C,UAAI,cAAc,iBAAiB;AACjC,wBAAgB,KAAK,OAAO,QAAQ;AAAA,MACtC;AACA,WAAK,SAAS,0CAAsB,EAAE,QAAQ,IAAI,QAAQ,OAAO,UAAU,CAAC;AAC5E,UAAI,CAAC,aAAa,IAAI,WAAW,KAAK;AACpC,cAAM,IAAI,SAAS,oBAAoB,yGAAyB;AAAA,MAClE;AACA,YAAM,IAAI;AAAA,QACR,uBAAuB,WAAW,SAAS;AAAA,QAC1C,QAAQ,qBAAgC,yCAAqB,IAAI,MAAM;AAAA,MAC1E;AAAA,IACF;AAEA,UAAM,OAAsB,MAAM,IAAI,KAAK;AAI3C,6BAAyB,MAAM,KAAK;AAEpC,QAAI,KAAK,UAAU;AACjB,UAAI;AAKF,cAAM;AAAA,UACJ,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK,OAAO;AAAA,UACZ,KAAK,OAAO;AAAA,QACd;AAAA,MACF,SAAS,KAAK;AACZ,wBAAgB,KAAK,OAAO,QAAQ;AACpC,cAAM;AAAA,MACR;AAAA,IACF;AAEA,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,YAAuB;AAAA,MAC3B,cAAc,KAAK;AAAA,MACnB,YAAY,KAAK;AAAA,MACjB,YAAY,KAAK;AAAA;AAAA,MAEjB,eAAe,KAAK,iBAAiB,QAAQ;AAAA,MAC7C,UAAU,KAAK;AAAA,MACf,WAAW;AAAA,MACX,YAAY,MAAM,KAAK,aAAa;AAAA,IACtC;AAEA,kBAAc,WAAW,KAAK,OAAO,QAAQ;AAC7C,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,cAAgC;AACpC,QAAI,YAAY,aAAa,KAAK,OAAO,QAAQ;AAEjD,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,SAAS,qBAAqB,oBAAK;AAAA,IAC/C;AAGA,QAAI,KAAK,IAAI,KAAK,UAAU,aAAa,gBAAgB;AACvD,kBAAY,MAAM,KAAK,aAAa;AAAA,IACtC;AAEA,UAAM,mBAAmB,MAAM,KAAK,qBAAqB;AAEzD,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,iBAAiB,kBAAkB;AAAA,QAC7C,SAAS;AAAA,UACP,eAAe,UAAU,UAAU,YAAY;AAAA,QACjD;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,YAAM,IAAI,SAAS,iBAAiB,4EAAgB,GAAG;AAAA,IACzD;AAEA,QAAI,CAAC,IAAI,IAAI;AACX,UAAI,IAAI,WAAW,KAAK;AACtB,wBAAgB,KAAK,OAAO,QAAQ;AACpC,cAAM,IAAI,SAAS,qBAAqB,0BAAW;AAAA,MACrD;AACA,YAAM,IAAI;AAAA,QACR;AAAA,QACA,0DAAkB,IAAI,MAAM;AAAA,MAC9B;AAAA,IACF;AAEA,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,iBAAyC;AAC7C,QAAI,YAAY,aAAa,KAAK,OAAO,QAAQ;AACjD,QAAI,CAAC,UAAW,QAAO;AAEvB,QAAI,KAAK,IAAI,KAAK,UAAU,aAAa,gBAAgB;AACvD,kBAAY,MAAM,KAAK,aAAa;AAAA,IACtC;AAEA,WAAO,UAAU;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,kBAA2B;AACzB,UAAM,YAAY,aAAa,KAAK,OAAO,QAAQ;AACnD,QAAI,CAAC,UAAW,QAAO;AACvB,WAAO,KAAK,IAAI,IAAI,UAAU;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,OAAO,gBAAyB,OAAsB;AAE1D,UAAM,YAAY,aAAa,KAAK,OAAO,QAAQ;AACnD,UAAM,eAAe,WAAW;AAChC,UAAM,cAAc,WAAW;AAE/B,oBAAgB,KAAK,OAAO,QAAQ;AACpC,SAAK,SAAS,sDAAmB;AAAA,MAC/B,UAAU,KAAK,OAAO;AAAA,MACtB;AAAA,IACF,CAAC;AAID,QAAI,gBAAgB,KAAK,OAAO,UAAU;AACxC,UAAI;AAGF,cAAM,YAAY,KAAK,OAAO,gBAAgB,OAAO,MAAM,KAAK,cAAc;AAC9E,cAAM,YAAY,KAAK,OAAO,gBAC1B,GAAG,KAAK,WAAW,sBACnB,WAAW,uBACX,GAAG,KAAK,OAAO,UAAU;AAC7B,cAAM,aAAa,IAAI,gBAAgB;AAAA,UACrC,OAAO;AAAA,UACP,iBAAiB;AAAA,UACjB,WAAW,KAAK,OAAO;AAAA,QACzB,CAAC;AACD,YAAI,KAAK,OAAO,cAAc;AAC5B,qBAAW,IAAI,iBAAiB,KAAK,OAAO,YAAY;AAAA,QAC1D;AACA,cAAM;AAAA,UACJ;AAAA,UACA;AAAA,YACE,QAAQ;AAAA,YACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,YAC/D,MAAM,WAAW,SAAS;AAAA,UAC5B;AAAA,UACA;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,QAAI,eAAe;AAEjB,YAAM,YAAY,MAAM,KAAK,cAAc;AAC3C,YAAM,qBACJ,WAAW,wBAAwB,GAAG,KAAK,OAAO,UAAU;AAC9D,YAAM,YAAY,IAAI,IAAI,kBAAkB;AAC5C,gBAAU,aAAa,IAAI,aAAa,KAAK,OAAO,QAAQ;AAC5D,YAAM,gBAAgB,KAAK,OAAO,yBAAyB,KAAK,OAAO;AACvE,UAAI,eAAe;AACjB,kBAAU,aAAa;AAAA,UACrB;AAAA,UACA;AAAA,QACF;AAAA,MACF;AACA,UAAI,aAAa;AACf,kBAAU,aAAa,IAAI,iBAAiB,WAAW;AAAA,MACzD;AAEA,YAAM,QAAQ,cAAc;AAC5B,sBAAgB,OAAO,KAAK,OAAO,QAAQ;AAC3C,gBAAU,aAAa,IAAI,SAAS,KAAK;AACzC,aAAO,SAAS,OAAO,UAAU,SAAS;AAAA,IAC5C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,oBAAoB,KAAsB;AACxC,UAAM,gBAAgB,IAAI,IAAI,GAAG,EAAE,aAAa,IAAI,OAAO;AAC3D,QAAI,CAAC,cAAe,QAAO;AAC3B,UAAM,aAAa,eAAe,KAAK,OAAO,QAAQ;AACtD,QAAI,CAAC,WAAY,QAAO;AACxB,QAAI,CAAC,sBAAsB,YAAY,aAAa,EAAG,QAAO;AAE9D,sBAAkB,KAAK,OAAO,QAAQ;AACtC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,eAA8C;AAClD,WAAO,KAAK,cAAc;AAAA,EAC5B;AACF;AAAA;AA18Ba,WAWa,mBAAmB,IAAI,KAAK;AAAA;AAXzC,WAca,uBAAuB;AAd1C,IAAM,YAAN;;;APxJP,IAAM,iBAAa,4BAAsC,IAAI;AAmC7D,IAAM,sBAAsB;AAC5B,IAAM,cAAc;AAIpB,IAAM,iBAAiB;AAGvB,IAAM,SAAS,GAAG,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;AAWzE,SAAS,kBACd,UACA,MACA,WACM;AACN,MAAI,OAAO,qBAAqB,YAAa;AAC7C,MAAI;AACF,UAAM,UAAU,IAAI,iBAAiB,iBAAiB,QAAQ;AAE9D,YAAQ,YAAY;AAAA,MAClB;AAAA,MACA,aAAa;AAAA,MACb,GAAI,SAAS,WAAW,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACvD,CAAC;AACD,YAAQ,MAAM;AAAA,EAChB,QAAQ;AAAA,EAER;AACF;AAGA,IAAM,aAAa,oBAAI,IAAoB;AAE3C,SAAS,QAAQ,UAA0B;AACzC,SAAO,sBAAsB;AAC/B;AAUA,SAAS,mBAAmB,UAA2B;AACrD,MAAI,OAAO,iBAAiB,YAAa,QAAO;AAChD,QAAM,MAAM,QAAQ,QAAQ;AAC5B,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,MAAM,aAAa,QAAQ,GAAG;AACpC,MAAI,KAAK;AACP,UAAM,KAAK,SAAS,KAAK,EAAE;AAC3B,QAAI,CAAC,MAAM,EAAE,KAAK,MAAM,KAAK,aAAa;AACxC,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,QAAQ,GAAG,GAAG,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;AAC3D,eAAa,QAAQ,KAAK,KAAK;AAE/B,MAAI,aAAa,QAAQ,GAAG,MAAM,MAAO,QAAO;AAChD,aAAW,IAAI,UAAU,KAAK;AAC9B,SAAO;AACT;AAEA,SAAS,mBAAmB,UAAwB;AAClD,MAAI,OAAO,iBAAiB,YAAa;AACzC,QAAM,QAAQ,WAAW,IAAI,QAAQ;AACrC,aAAW,OAAO,QAAQ;AAE1B,MAAI,SAAS,aAAa,QAAQ,QAAQ,QAAQ,CAAC,MAAM,OAAO;AAC9D,iBAAa,WAAW,QAAQ,QAAQ,CAAC;AAAA,EAC3C;AACF;AAeA,eAAsB,gBAAgB,UAAkB,MAA6C;AACnG,MACE,OAAO,cAAc,eACrB,OAAO,UAAU,OAAO,YAAY,YACpC;AACA,QAAI;AACF,UAAI,MAAM;AACV,YAAM,UAAU,MAAM;AAAA,QACpB,uBAAuB,QAAQ;AAAA,QAC/B,EAAE,aAAa,KAAK;AAAA,QACpB,OAAO,SAAS;AACd,cAAI,CAAC,KAAM;AACX,gBAAM;AACN,gBAAM,KAAK;AAAA,QACb;AAAA,MACF;AACA,aAAO;AAAA,IACT,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,CAAC,mBAAmB,QAAQ,EAAG,QAAO;AAC1C,MAAI;AACF,UAAM,KAAK;AACX,WAAO;AAAA,EACT,UAAE;AACA,uBAAmB,QAAQ;AAAA,EAC7B;AACF;AAMO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,mBAAmB;AAAA,EACnB;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,CAAC,MAAM,OAAO,QAAI,uBAAyB,IAAI;AACrD,QAAM,CAAC,WAAW,YAAY,QAAI,uBAAS,IAAI;AAC/C,QAAM,CAAC,iBAAiB,kBAAkB,QAAI,uBAAS,KAAK;AAC5D,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAA0B,IAAI;AACxD,QAAM,CAAC,gBAAgB,iBAAiB,QAAI,uBAAS,KAAK;AAE1D,QAAM,CAAC,MAAM,QAAI,uBAAS,MAAM,IAAI,UAAU,MAAM,CAAC;AACrD,QAAM,sBAAkB,qBAA6C,IAAI;AACzE,QAAM,gBAAY,qBAAO,KAAK;AAE9B,QAAM,0BAAsB,qBAAO,gBAAgB;AACnD,8BAAU,MAAM;AACd,wBAAoB,UAAU;AAAA,EAChC,CAAC;AAED,QAAM,gCAA4B,qBAAO,KAAK;AAE9C,QAAM,6BAAyB,qBAAO,CAAC;AACvC,QAAM,uBAAuB;AAG7B,QAAM,yBAAqB,0BAAY,CAACA,WAAoB;AAC1D,aAASA,MAAK;AACd,sBAAkB,IAAI;AACtB,QAAI,CAAC,0BAA0B,SAAS;AACtC,gCAA0B,UAAU;AACpC,0BAAoB,UAAUA,MAAK;AAAA,IACrC;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,QAAM,eAAW,0BAAY,YAAY;AAIvC,UAAM,YAAY,aAAa,OAAO,OAAO,QAAQ;AACrD,QAAI,CAAC,WAAW;AACd,cAAQ,IAAI;AACZ,yBAAmB,KAAK;AAGxB,mBAAa,KAAK;AAClB;AAAA,IACF;AAEA,QAAI;AACF,YAAM,IAAI,MAAM,OAAO,YAAY;AACnC,cAAQ,CAAC;AACT,yBAAmB,IAAI;AACvB,eAAS,IAAI;AACb,wBAAkB,KAAK;AACvB,gCAA0B,UAAU;AACpC,6BAAuB,UAAU;AAAA,IACnC,SAAS,KAAK;AACZ,cAAQ,IAAI;AACZ,yBAAmB,KAAK;AACxB,YAAM,WACJ,eAAe,WACX,MACA,IAAI,SAAS,mBAAmB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAEtF,eAAS,QAAQ;AAGjB,UAAI,SAAS,SAAS,uBAAuB,SAAS,SAAS,mBAAmB;AAChF,wBAAgB,OAAO,OAAO,QAAQ;AACtC,2BAAmB,QAAQ;AAAA,MAC7B;AAAA,IACF,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,QAAQ,kBAAkB,CAAC;AAG/B,8BAAU,MAAM;AACd,QAAI,UAAU,QAAS;AACvB,cAAU,UAAU;AAEpB,YAAQ,QAAQ,EAAE,KAAK,MAAM;AAE3B,UAAI,aAAa,OAAO,OAAO,QAAQ,GAAG;AACxC,iBAAS;AAAA,MACX,OAAO;AACL,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF,CAAC;AAAA,EACH,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAGrB,8BAAU,MAAM;AACd,QAAI,SAAS;AAIb,UAAM,iBAAiB,MACrB,gBAAgB,OAAO,OAAO,UAAU,YAAY;AAClD,UAAI;AACF,cAAM,KAAK,MAAM,OAAO,aAAa;AACrC,+BAAuB,UAAU;AACjC,2BAAmB,GAAG,YAAY;AAGlC,0BAAkB,OAAO,OAAO,UAAU,SAAS,EAAE;AACrD,iBAAS;AAAA,MACX,SAAS,KAAK;AAGZ,YACE,eAAe,aACd,IAAI,SAAS,qBACZ,IAAI,SAAS,sBACb,IAAI,SAAS,sBACf;AACA,0BAAgB,OAAO,OAAO,QAAQ;AACtC,6BAAmB,GAAG;AAAA,QACxB,OAAO;AAGL,iCAAuB,WAAW;AAClC,cAAI,uBAAuB,WAAW,sBAAsB;AAC1D;AAAA,cACE,eAAe,WACX,MACA,IAAI,SAAS,mBAAmB,0EAAc;AAAA,YACpD;AAAA,UACF;AAAA,QACF;AACA,mBAAW,MAAM,SAAS,GAAG,GAAG;AAAA,MAClC;AAAA,IACF,CAAC,EAAE,QAAQ,MAAM;AAKf,UAAI,OAAQ,qBAAoB,GAAK;AAAA,IACvC,CAAC;AAEH,UAAM,sBAAsB,CAAC,aAAa,MAAM;AAC9C,UAAI,CAAC,OAAQ;AAGb,UAAI,gBAAgB,SAAS;AAC3B,qBAAa,gBAAgB,OAAO;AACpC,wBAAgB,UAAU;AAAA,MAC5B;AAEA,YAAM,YAAY,aAAa,OAAO,OAAO,QAAQ;AACrD,UAAI,CAAC,UAAW;AAGhB,YAAM,gBAAgB,UAAU,aAAa,KAAK,IAAI,KAAK;AAG3D,UAAI,gBAAgB,KAAK,gBAAgB,kBAAkB;AACzD,wBAAgB,UAAU;AAAA,UACxB,MAAM;AACJ,gBAAI,OAAQ,MAAK,eAAe;AAAA,UAClC;AAAA,UACA,KAAK,IAAI,YAAY,GAAI;AAAA,QAC3B;AACA;AAAA,MACF;AAGA,YAAM,UAAU,KAAK,KAAK,eAAe,oBAAoB,KAAM,YAAY,GAAI;AAEnF,sBAAgB,UAAU,WAAW,MAAM;AACzC,YAAI,CAAC,OAAQ;AACb,cAAM,KAAK,aAAa,OAAO,OAAO,QAAQ;AAC9C,YAAI,CAAC,GAAI;AAET,cAAM,WAAW,GAAG,aAAa,KAAK,IAAI,KAAK;AAC/C,YAAI,WAAW,kBAAkB;AAC/B,eAAK,eAAe;AAAA,QACtB,OAAO;AAEL,8BAAoB;AAAA,QACtB;AAAA,MACF,GAAG,OAAO;AAAA,IACZ;AAEA,wBAAoB;AAEpB,WAAO,MAAM;AACX,eAAS;AACT,UAAI,gBAAgB,SAAS;AAC3B,qBAAa,gBAAgB,OAAO;AACpC,wBAAgB,UAAU;AAAA,MAC5B;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,UAAU,kBAAkB,gBAAgB,CAAC;AAGzD,8BAAU,MAAM;AACd,UAAM,sBAAsB,CAAC,MAAoB;AAC/C,UACE,EAAE,OACF,EAAE,IAAI,WAAW,cAAc,KAC/B,CAAC,EAAE,IAAI,WAAW,mBAAmB,GACrC;AACA,iBAAS;AAAA,MACX;AAAA,IACF;AAEA,WAAO,iBAAiB,WAAW,mBAAmB;AACtD,WAAO,MAAM,OAAO,oBAAoB,WAAW,mBAAmB;AAAA,EACxE,GAAG,CAAC,QAAQ,CAAC;AAQb,8BAAU,MAAM;AACd,QAAI,OAAO,qBAAqB,YAAa;AAC7C,UAAM,UAAU,IAAI,iBAAiB,iBAAiB,OAAO,OAAO,QAAQ;AAC5E,YAAQ,YAAY,CAAC,UAAwB;AAC3C,YAAM,OAAO,MAAM;AAKnB,UAAI,CAAC,QAAQ,KAAK,gBAAgB,OAAQ;AAC1C,UAAI,KAAK,SAAS,UAAU;AAC1B,wBAAgB,OAAO,OAAO,QAAQ;AACtC,gBAAQ,IAAI;AACZ,2BAAmB,KAAK;AACxB,iBAAS,IAAI;AACb,0BAAkB,KAAK;AACvB,kCAA0B,UAAU;AACpC,+BAAuB,UAAU;AAAA,MACnC,WAAW,KAAK,SAAS,SAAS;AAChC,cAAM,KAAK,KAAK;AAChB,YACE,MACA,OAAO,GAAG,iBAAiB,YAC3B,OAAO,GAAG,eAAe,YACzB,OAAO,GAAG,kBAAkB,UAC5B;AAEA,wBAAc,IAAiB,OAAO,OAAO,QAAQ;AAAA,QACvD;AAEA,iBAAS;AAAA,MACX;AAAA,IACF;AACA,WAAO,MAAM,QAAQ,MAAM;AAAA,EAC7B,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAGrB,QAAM,YAAQ;AAAA,IACZ,OAAO,cAAuB;AAC5B,YAAM,OAAO,MAAM,SAAS;AAAA,IAC9B;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAGA,QAAM,iBAAa;AAAA,IACjB,OAAO,YAAsE;AAC3E,YAAM,YAAY,MAAM,OAAO,WAAW,OAAO;AACjD,YAAM,SAAS;AACf,aAAO;AAAA,IACT;AAAA,IACA,CAAC,QAAQ,QAAQ;AAAA,EACnB;AAGA,QAAM,aAAS;AAAA,IACb,OAAO,gBAAyB,UAAU;AACxC,cAAQ,IAAI;AACZ,yBAAmB,KAAK;AACxB,eAAS,IAAI;AACb,wBAAkB,KAAK;AACvB,gCAA0B,UAAU;AACpC,6BAAuB,UAAU;AAEjC,wBAAkB,OAAO,OAAO,UAAU,QAAQ;AAClD,UAAI;AACF,cAAM,OAAO,OAAO,aAAa;AAAA,MACnC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAGA,QAAM,kBAAc,0BAAY,YAAY;AAC1C,UAAM,SAAS;AAAA,EACjB,GAAG,CAAC,QAAQ,CAAC;AAGb,QAAM,qBAAiB,0BAAY,YAAoC;AACrE,WAAO,OAAO,eAAe;AAAA,EAC/B,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,QAAyB;AAAA,IAC7B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO,aAAAC,QAAM,cAAc,WAAW,UAAU,EAAE,MAAM,GAAG,QAAQ;AACrE;AAoBO,SAAS,SAA0B;AACxC,QAAM,UAAM,yBAAW,UAAU;AACjC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,oEAAiC;AAAA,EACnD;AACA,SAAO;AACT;;;AQjkBA,IAAAC,gBAAuE;AAmDhE,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,WAAW;AAAA,EACX;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,EAAE,iBAAiB,WAAW,OAAO,WAAW,IAAI,OAAO;AACjE,QAAM,wBAAoB,sBAAO,KAAK;AACtC,QAAM,CAAC,YAAY,aAAa,QAAI,wBAAkB,IAAI;AAE1D,QAAM,eAAe,MAAM;AACzB,sBAAkB,UAAU;AAC5B,UAAM,cAAc,OAAO,SAAS,WAAW,OAAO,SAAS;AAE/D,UAAM,gBAAgB,CAAC,QAAiB;AAEtC,wBAAkB,UAAU;AAC5B,oBAAc,GAAG;AACjB,gBAAU,GAAG;AAAA,IACf;AAEA,QAAI,UAAU;AACZ,iBAAW,EAAE,WAAW,YAAY,CAAC,EAAE,MAAM,CAAC,QAAQ;AAEpD,YAAK,IAA0B,SAAS,iBAAiB;AACvD,gBAAM,WAAW,EAAE,MAAM,aAAa;AACtC;AAAA,QACF;AAEA,sBAAc,GAAG;AAAA,MACnB,CAAC;AAAA,IACH,OAAO;AACL,YAAM,WAAW,EAAE,MAAM,aAAa;AAAA,IACxC;AAAA,EACF;AAEA,QAAM,QAAQ,MAAM;AAClB,kBAAc,IAAI;AAClB,iBAAa;AAAA,EACf;AAEA,+BAAU,MAAM;AACd,QAAI,CAAC,aAAa,CAAC,mBAAmB,aAAa,CAAC,kBAAkB,SAAS;AAC7E,mBAAa;AAAA,IACf;AAAA,EAEF,GAAG,CAAC,WAAW,iBAAiB,WAAW,UAAU,OAAO,UAAU,CAAC;AAEvE,MAAI,WAAW;AACb,WACE,YAAY,cAAAC,QAAM,cAAc,OAAO,MAAM,qDAAa;AAAA,EAE9D;AAEA,MAAI,CAAC,iBAAiB;AACpB,QAAI,YAAY;AACd,UAAI,kBAAkB;AACpB,eAAO,cAAAA,QAAM;AAAA,UACX,cAAAA,QAAM;AAAA,UACN;AAAA,UACA,iBAAiB,YAAY,KAAK;AAAA,QACpC;AAAA,MACF;AACA,YAAM,UACJ,sBAAsB,QAAQ,WAAW,UAAU;AACrD,aAAO,cAAAA,QAAM;AAAA,QACX;AAAA,QACA;AAAA,QACA,cAAAA,QAAM,cAAc,KAAK,MAAM,uCAAS,OAAO,EAAE;AAAA,QACjD,cAAAA,QAAM,cAAc,UAAU,EAAE,SAAS,MAAM,GAAG,0BAAM;AAAA,MAC1D;AAAA,IACF;AACA,QAAI,SAAU,QAAO,cAAAA,QAAM,cAAc,cAAAA,QAAM,UAAU,MAAM,QAAQ;AACvE,WAAO,cAAAA,QAAM,cAAc,OAAO,MAAM,0BAAM;AAAA,EAChD;AAEA,SAAO,cAAAA,QAAM,cAAc,cAAAA,QAAM,UAAU,MAAM,QAAQ;AAC3D;AAeO,SAAS,SACd,WACkB;AAClB,QAAM,cACJ,UAAU,eAAe,UAAU,QAAQ;AAE7C,WAAS,iBAAiB,OAAU;AAClC,WAAO,cAAAA,QAAM;AAAA,MACX;AAAA,MACA;AAAA,MACA,cAAAA,QAAM,cAAc,WAAW,KAAK;AAAA,IACtC;AAAA,EACF;AAEA,mBAAiB,cAAc,YAAY,WAAW;AACtD,SAAO;AACT;;;ACnJA,IAAAC,gBAAmD;AAuBnD,IAAM,uBAAwD;AAAA,EAC5D,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,2BAA2B;AAAA,EAC3B,iBAAiB;AAAA,EACjB,4BAA4B;AAAA,EAC5B,yBAAyB;AAAA,EACzB,sBAAsB;AAAA,EACtB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,cAAc;AAAA,EACd,eAAe;AAAA,EACf,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,kBAAkB;AACpB;AAGO,SAAS,wBAAwB,KAAsB;AAC5D,MAAI,eAAe,UAAU;AAC3B,UAAM,OAAO,qBAAqB,IAAI,IAAI;AAC1C,QAAI,KAAM,QAAO;AACjB,QAAI,IAAI,KAAK,WAAW,UAAU,EAAG,QAAO;AAE5C,QAAI,kBAAkB,KAAK,IAAI,WAAW,EAAG,QAAO,IAAI;AACxD,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGO,SAAS,qBAAqB,EAAE,MAAM,GAAsB;AACjE,SAAO,cAAAC,QAAM;AAAA,IACX;AAAA,IACA;AAAA,MACE,OAAO;AAAA,QACL,SAAS;AAAA,QACT,eAAe;AAAA,QACf,YAAY;AAAA,QACZ,gBAAgB;AAAA,QAChB,WAAW;AAAA,QACX,YAAY;AAAA,QACZ,SAAS;AAAA,QACT,WAAW;AAAA,MACb;AAAA,IACF;AAAA,IACA,cAAAA,QAAM;AAAA,MACJ;AAAA,MACA,EAAE,OAAO,EAAE,OAAO,WAAW,cAAc,SAAS,EAAE;AAAA,MACtD;AAAA,IACF;AAAA,IACA,cAAAA,QAAM;AAAA,MACJ;AAAA,MACA,EAAE,OAAO,EAAE,SAAS,QAAQ,KAAK,UAAU,EAAE;AAAA,MAC7C,cAAAA,QAAM;AAAA,QACJ;AAAA,QACA;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,YACL,SAAS;AAAA,YACT,SAAS;AAAA,YACT,YAAY;AAAA,YACZ,OAAO;AAAA,YACP,UAAU;AAAA,YACV,gBAAgB;AAAA,UAClB;AAAA,QACF;AAAA,QACA;AAAA,MACF;AAAA,MACA,cAAAA,QAAM;AAAA,QACJ;AAAA,QACA;AAAA,UACE,MAAM;AAAA,UACN,SAAS,MAAM;AACb,gBAAI,OAAO,QAAQ,SAAS,EAAG,QAAO,QAAQ,KAAK;AAAA,gBAC9C,QAAO,SAAS,OAAO;AAAA,UAC9B;AAAA,UACA,OAAO;AAAA,YACL,SAAS;AAAA,YACT,SAAS;AAAA,YACT,QAAQ;AAAA,YACR,YAAY;AAAA,YACZ,OAAO;AAAA,YACP,UAAU;AAAA,YACV,QAAQ;AAAA,UACV;AAAA,QACF;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,aAAa,EAAE,WAAW,SAAS,YAAY,IAAuB,CAAC,GAAG;AACxF,QAAM,EAAE,QAAQ,YAAY,IAAI,OAAO;AACvC,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAAwB,IAAI;AACtD,QAAM,CAAC,YAAY,aAAa,QAAI,wBAAS,IAAI;AAGjD,QAAM,CAAC,qBAAqB,sBAAsB,QAAI,wBAAS,KAAK;AAKpE,QAAM,mBAAe,sBAAO,SAAS;AACrC,QAAM,iBAAa,sBAAO,OAAO;AACjC,+BAAU,MAAM;AACd,iBAAa,UAAU;AACvB,eAAW,UAAU;AAAA,EACvB,CAAC;AAMD,QAAM,yBAAqB,sBAAyC,IAAI;AAExE,QAAM,uBAAmB,sBAAO,KAAK;AACrC,QAAM,sBAAkB,sBAAO,KAAK;AAEpC,+BAAU,MAAM;AAGd,UAAM,aAAa,IAAI,IAAI,OAAO,SAAS,IAAI,EAAE,aAAa,IAAI,aAAa;AAC/E,QAAI,OAAO,UAAU,CAAC,OAAO,OAAO,UAAU,YAAY;AACxD,YAAM,QAAQ;AAEd,UAAI,eAAe,OAAO,SAAS;AACnC,UAAI;AACF,uBAAe,OAAO,OAAO,SAAS;AAAA,MACxC,QAAQ;AAAA,MAER;AAIA,YAAM,UAAU;AAAA,QACd,MAAM;AAAA,QACN,aAAa,OAAO,SAAS;AAAA,QAC7B;AAAA,MACF;AACA,UAAI,QAAQ;AACZ,YAAM,OAAO,MAAM;AACjB,YAAI,SAAS,CAAC,OAAO,UAAU,OAAO,OAAO,OAAQ;AACrD,eAAO,OAAO,YAAY,SAAS,YAAY;AAAA,MACjD;AACA,WAAK;AACL,YAAM,cAAc,YAAY,MAAM,GAAG;AACzC,YAAM,aAAa,WAAW,MAAM;AAClC,YAAI,MAAO;AACX,sBAAc,WAAW;AACzB,+BAAuB,IAAI;AAAA,MAC7B,GAAG,GAAM;AAET,YAAM,YAAY,CAAC,UAAwB;AAEzC,YAAI,MAAM,WAAW,OAAO,OAAQ;AACpC,YAAI,MAAM,WAAW,aAAc;AACnC,YAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,SAAS,wBAAyB;AAChE,YAAI,MAAM,KAAK,UAAU,MAAO;AAChC,gBAAQ;AACR,sBAAc,WAAW;AACzB,qBAAa,UAAU;AAAA,MACzB;AACA,aAAO,iBAAiB,WAAW,SAAS;AAG5C,cAAQ,QAAQ,EAAE,KAAK,MAAM,cAAc,KAAK,CAAC;AAEjD,aAAO,MAAM;AACX,sBAAc,WAAW;AACzB,qBAAa,UAAU;AACvB,eAAO,oBAAoB,WAAW,SAAS;AAAA,MACjD;AAAA,IACF;AAEA,QAAI,YAAY;AAEhB,mBAAe,iBAAiB;AAC9B,UAAI;AACF,YAAI,CAAC,mBAAmB,SAAS;AAC/B,6BAAmB,UAAU,OAAO,eAAe,OAAO,SAAS,IAAI;AAAA,QACzE;AACA,cAAM,YAAY,MAAM,mBAAmB;AAE3C,YAAI,aAAa,iBAAiB,QAAS;AAC3C,yBAAiB,UAAU;AAI3B,YAAI,cAAc,MAAM;AACtB,gBAAMC,YAAW,OAAO,OAAO;AAC/B,gBAAM,iBAAiB,aAAaA,SAAQ;AAC5C,0BAAgBA,SAAQ;AACxB,gBAAM,SACJ,kBAAkB,mBAAmB,gBAAgB,OAAO,SAAS,MAAM,IACvE,iBACA;AACN,iBAAO,SAAS,OACd,UAAU,OAAO,SAAS,GAAG,IAAI,MAAM,OAAO;AAChD;AAAA,QACF;AAGA,cAAM,YAAY;AAElB,YAAI,UAAW;AAGf,cAAM,WAAW,OAAO,OAAO;AAC/B,cAAM,YAAY,aAAa,QAAQ;AACvC,wBAAgB,QAAQ;AAGxB,cAAM,cAAc,aAAa;AACjC,YAAI,aAAa;AACf,sBAAY,SAAS;AACrB;AAAA,QACF;AAIA,eAAO,SAAS,OACd,aAAa,mBAAmB,WAAW,OAAO,SAAS,MAAM,IAC7D,YACA;AAAA,MACR,SAAS,KAAK;AACZ,YAAI,aAAa,gBAAgB,QAAS;AAC1C,wBAAgB,UAAU;AAE1B,2BAAmB,UAAU;AAC7B,cAAM,WAAW,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AACnE,mBAAW,UAAU,QAAQ;AAC7B,iBAAS,wBAAwB,QAAQ,CAAC;AAC1C,sBAAc,KAAK;AAAA,MACrB;AAAA,IACF;AAEA,mBAAe;AAEf,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAM;AAAA,EACnC,GAAG,CAAC,QAAQ,WAAW,CAAC;AAExB,MAAI,OAAO;AACT,QAAI,YAAa,QAAO,cAAAD,QAAM,cAAc,cAAAA,QAAM,UAAU,MAAM,YAAY,KAAK,CAAC;AACpF,WAAO,cAAAA,QAAM,cAAc,sBAAsB,EAAE,MAAM,CAAC;AAAA,EAC5D;AAEA,MAAI,qBAAqB;AACvB,WAAO,cAAAA,QAAM;AAAA,MACX;AAAA,MACA;AAAA,QACE,OAAO;AAAA,UACL,SAAS;AAAA,UACT,YAAY;AAAA,UACZ,gBAAgB;AAAA,UAChB,WAAW;AAAA,UACX,YAAY;AAAA,QACd;AAAA,MACF;AAAA,MACA,cAAAA,QAAM,cAAc,KAAK,MAAM,sFAAgB;AAAA,IACjD;AAAA,EACF;AAEA,MAAI,YAAY;AACd,WAAO,cAAAA,QAAM;AAAA,MACX;AAAA,MACA;AAAA,QACE,OAAO;AAAA,UACL,SAAS;AAAA,UACT,YAAY;AAAA,UACZ,gBAAgB;AAAA,UAChB,WAAW;AAAA,UACX,YAAY;AAAA,QACd;AAAA,MACF;AAAA,MACA,cAAAA,QAAM,cAAc,KAAK,MAAM,qDAAa;AAAA,IAC9C;AAAA,EACF;AAEA,SAAO;AACT;","names":["error","React","import_react","React","import_react","React","clientId"]}