@nihplod/sso-sdk 1.0.1 → 1.1.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.
package/README.md CHANGED
@@ -260,6 +260,11 @@ export const middleware = createSsoMiddleware({
260
260
  publicPaths: ["/", "/public"],
261
261
  // Confidential Client (BFF) can pass clientSecret
262
262
  // clientSecret: process.env.SSO_CLIENT_SECRET,
263
+ // validateSsoCookie defaults to true: the middleware calls the introspection
264
+ // endpoint to verify the SSO session cookie. Set to false only if you accept
265
+ // "cookie exists = logged in" semantics (lowest latency, but may pass revoked
266
+ // sessions). Either way, the middleware is only a UX gate — always re-verify
267
+ // tokens in Route Handlers / Server Components before serving sensitive data.
263
268
  });
264
269
 
265
270
  export const config = {
@@ -284,21 +289,30 @@ export const GET = createCallbackRouteHandler({
284
289
  // src/app/api/auth/logout/route.ts
285
290
  import { createLogoutRouteHandler } from "@nihplod/sso-sdk/next";
286
291
 
287
- export const GET = createLogoutRouteHandler({
292
+ const handler = createLogoutRouteHandler({
288
293
  clientId: "...",
289
294
  ssoBaseUrl: "https://nihplod.cn",
290
295
  redirectUri: "https://yourapp.com/api/auth/callback",
291
296
  postLogoutRedirectUri: "https://yourapp.com/",
292
297
  redirectToSso: true,
293
298
  });
299
+
300
+ // Prefer POST to trigger logout (prevents logout CSRF via cross-site GET);
301
+ // GET is kept for compatibility with plain <a> navigations.
302
+ export const GET = handler;
303
+ export const POST = handler;
294
304
  ```
295
305
 
296
- Use a standard `<a>` tag to trigger the logout endpoint:
306
+ Trigger the logout endpoint with a POST request (recommended):
297
307
 
298
308
  ```tsx
299
- <a href="/api/auth/logout">Logout</a>
309
+ <button onClick={() => fetch("/api/auth/logout", { method: "POST" }).then(() => location.assign("/"))}>
310
+ Logout
311
+ </button>
300
312
  ```
301
313
 
314
+ A plain `<a href="/api/auth/logout">` still works via GET, but note it can be triggered cross-site (logout CSRF).
315
+
302
316
  ### Cookie Configuration
303
317
 
304
318
  Default cookie names:
@@ -319,6 +333,8 @@ Default cookie names:
319
333
 
320
334
  By default, the SDK stores tokens in **memory**. Public Clients (SPA / mobile / desktop) should **never** write the `refresh_token` to `localStorage` to prevent XSS from stealing long-lived credentials.
321
335
 
336
+ Transient OAuth data (PKCE `code_verifier`, `state`, `returnUrl`, popup nonce) is stored separately in **sessionStorage**, because it must survive the full-page redirect to the SSO center and back; it is cleared automatically when the tab closes. In SSR environments without `sessionStorage`, it falls back to an in-memory map.
337
+
322
338
  If the sub-project is a **Next.js BFF / Confidential Client**, you can store tokens in `localStorage` for multi-tab sharing:
323
339
 
324
340
  ```typescript
package/dist/index.d.mts CHANGED
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * Token 存储抽象层
3
3
  *
4
- * 默认使用 localStorage,以支持多 Tab 间自动同步 token 并避免并发刷新。
5
- * 对 XSS 敏感的子项目可通过 setTokenStorage() 注入更安全的自定义实现
6
- *(如内存存储、Service Worker 封装、或带加密的 storage)。
4
+ * 分两类存储:
5
+ * - Token 数据:默认使用内存存储,Public Client 浏览器中 refresh_token 不落盘,
6
+ * XSS 无法窃取长期凭证。对需要多 Tab 共享 token 或 BFF/Confidential Client 场景,
7
+ * 可通过 setTokenStorage() 注入 localStorage 实现(如 createSecureStorage({ persist: true }))。
8
+ * - 临时数据(PKCE verifier / state / returnUrl / popup nonce):必须跨整页重定向存活
9
+ * (login() 会 302 跳转到 SSO 中心再回来),因此默认写入 sessionStorage;
10
+ * SSR 等无 sessionStorage 环境自动降级为内存 Map。
7
11
  *
8
12
  * 多 client 隔离:
9
13
  * - token / state / return_url 均支持按 clientId 隔离 key
@@ -27,6 +31,19 @@ interface TokenStorage {
27
31
  set(key: string, value: string): void;
28
32
  remove(key: string): void;
29
33
  }
34
+ /**
35
+ * 创建存储实现
36
+ *
37
+ * @param options.persist 是否持久化到 localStorage。默认 false(内存存储)。
38
+ * ⚠️ 安全警告:persist=true 会将 refresh_token 明文写入 localStorage,
39
+ * 任何 XSS 均可在不被检测的情况下读取。仅在 BFF/Confidential Client
40
+ * 且 refresh_token 不直接暴露给浏览器的场景下使用。
41
+ * - Confidential/BFF 子项目可设为 true。
42
+ * - Public Client 在浏览器中应保持 false(默认)。
43
+ */
44
+ declare function createSecureStorage(options?: {
45
+ persist?: boolean;
46
+ }): TokenStorage;
30
47
  /**
31
48
  * 设置自定义存储实现
32
49
  */
@@ -85,6 +102,8 @@ interface SsoClientConfig {
85
102
  ssoBaseUrl: string;
86
103
  /** 请求的 scope(空格分隔),如 "openid profile phone" */
87
104
  scopes?: string;
105
+ /** RP-Initiated Logout 返回地址(可选)。不传时回退到 redirectUri */
106
+ postLogoutRedirectUri?: string;
88
107
  }
89
108
  /** 用户信息 */
90
109
  interface SsoUser {
@@ -141,13 +160,19 @@ declare class SsoClient {
141
160
  private _getTokenEndpoint;
142
161
  /** 获取 userinfo 端点 URL(优先 Discovery,回退默认) */
143
162
  private _getUserinfoEndpoint;
163
+ /**
164
+ * 开放重定向防护:仅保存相对路径或与当前页面同源的 returnUrl,
165
+ * 其余(如 https://evil.com)忽略并告警,防止回调后跳转到钓鱼站点。
166
+ */
167
+ private _saveReturnUrlIfTrusted;
144
168
  /**
145
169
  * 发起 SSO 登录
146
170
  *
147
171
  * 生成 PKCE code_verifier/code_challenge 和 state 参数,
148
172
  * 构建 authorize URL,通过 302 跳转到 SSO 登录页。
149
173
  *
150
- * @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage)
174
+ * @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage;
175
+ * 仅允许相对路径或同源绝对 URL,否则忽略并告警)
151
176
  */
152
177
  login(returnUrl?: string): Promise<void>;
153
178
  /**
@@ -156,11 +181,49 @@ declare class SsoClient {
156
181
  * 适用于需要手动处理跳转的场景。
157
182
  */
158
183
  getLoginUrl(returnUrl?: string): Promise<string>;
184
+ /**
185
+ * 弹窗模式 SSO 登录
186
+ *
187
+ * 打开一个小窗口进行登录,认证完成后窗口自动关闭,
188
+ * 主页面不丢失状态(适用于 SPA 中需要保持表单/浏览上下文的场景)。
189
+ *
190
+ * 流程:
191
+ * 1. window.open() 打开授权 URL 到弹窗
192
+ * 2. 用户在弹窗中完成登录
193
+ * 3. 弹窗加载 CallbackPage 时检测到 window.opener,通过 postMessage 回传回调 URL
194
+ * 4. 主页面收到消息后调用 handleCallback() 交换 token
195
+ * 5. 弹窗自动关闭
196
+ *
197
+ * @param options - 弹窗配置
198
+ * @param options.returnUrl - 登录成功后的返回地址
199
+ * @param options.width - 弹窗宽度(默认 480)
200
+ * @param options.height - 弹窗高度(默认 640)
201
+ * @returns TokenData
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * const client = new SsoClient({ clientId: "xxx", redirectUri: "https://myapp.com/callback", ssoBaseUrl: "https://nihplod.cn" });
206
+ * try {
207
+ * const tokenData = await client.loginPopup({ returnUrl: "/dashboard" });
208
+ * console.log("登录成功", tokenData);
209
+ * } catch (err) {
210
+ * if (err instanceof SsoError && err.code === "popup_blocked") {
211
+ * // 弹窗被拦截,回退到同页重定向
212
+ * await client.login("/dashboard");
213
+ * }
214
+ * }
215
+ * ```
216
+ */
217
+ loginPopup(options?: {
218
+ returnUrl?: string;
219
+ width?: number;
220
+ height?: number;
221
+ }): Promise<TokenData>;
159
222
  /**
160
223
  * 处理 OAuth 回调
161
224
  *
162
225
  * 解析回调 URL,校验 state 参数,用授权码交换 token。
163
- * 成功后 token 自动保存到 sessionStorage。
226
+ * 成功后 token 自动保存到 token 存储(默认内存,可通过 setTokenStorage 定制)。
164
227
  *
165
228
  * @param callbackUrl - 完整的回调 URL(window.location.href)
166
229
  * @returns TokenData 或 null
@@ -247,9 +310,16 @@ declare function generateState(): string;
247
310
  * SSO SDK 错误类型
248
311
  */
249
312
  /** SSO 错误码 */
250
- type SsoErrorCode = "invalid_config" | "state_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "sso_server_error" | "network_error" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch";
251
- /** OAuth 2.0 标准错误码 */
252
- type OAuthErrorCode = "invalid_request" | "invalid_client" | "invalid_grant" | "unauthorized_client" | "unsupported_grant_type" | "invalid_scope" | "access_denied" | "server_error" | "rate_limited";
313
+ type SsoErrorCode = "invalid_config" | "state_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch";
314
+ /**
315
+ * 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode
316
+ *
317
+ * @param oauthError 服务端返回的 error 字段
318
+ * @param context 调用上下文:"token_exchange"(授权码换 token)或 "refresh"(刷新)。
319
+ * invalid_grant 在换 token 场景多为授权码过期/已用,在刷新场景多为会话失效,
320
+ * 需按上下文细化映射。
321
+ */
322
+ declare function mapOAuthErrorToSsoCode(oauthError: string, context?: "token_exchange" | "refresh"): SsoErrorCode;
253
323
  /**
254
324
  * SSO SDK 自定义错误
255
325
  */
@@ -260,13 +330,30 @@ declare class SsoError extends Error {
260
330
  constructor(code: SsoErrorCode, description: string, cause?: unknown);
261
331
  }
262
332
  /**
263
- * OAuth 2.0 服务端返回的错误
333
+ * OAuth 2.0 协议层错误(RFC 6749 §5.2)
334
+ *
335
+ * 用于表示服务端原样返回的 OAuth 错误(error / error_description / error_uri),
336
+ * 与 SDK 语义的 SsoError 区分。
264
337
  */
265
338
  declare class OAuthError extends Error {
266
- readonly code: OAuthErrorCode;
339
+ readonly code: string;
267
340
  readonly description: string;
268
341
  readonly uri?: string;
269
- constructor(code: OAuthErrorCode, description: string, uri?: string);
342
+ constructor(code: string, description: string, uri?: string);
270
343
  }
271
344
 
272
- export { OAuthError, type OAuthErrorCode, type OidcDiscovery, SsoClient, type SsoClientConfig, SsoError, type SsoErrorCode, type SsoUser, type TokenData, type TokenResponse, type TokenStorage, clearAllSsoData, clearVerifiersForClients, generateCodeChallenge, generateCodeVerifier, generateState, getOAuthState, getPkceVerifier, getReturnUrl, getTokenData, getTokenStorage, removeOAuthState, removePkceVerifier, removeReturnUrl, removeTokenData, saveOAuthState, savePkceVerifier, saveReturnUrl, saveTokenData, setTokenStorage };
345
+ /**
346
+ * 安全相关小工具:returnUrl 开放重定向校验、常量时间字符串比较
347
+ */
348
+ /**
349
+ * 校验 returnUrl 是否可信(防开放重定向)。
350
+ * 仅允许:相对路径(且不以 // 开头)或与 currentOrigin 完全同源的绝对 URL。
351
+ */
352
+ declare function isTrustedReturnUrl(url: string, currentOrigin: string): boolean;
353
+ /**
354
+ * 常量时间字符串比较(避免 state / at_hash / nonce 等机密值的时序侧信道)。
355
+ * 长度不同也执行完整循环,不提前返回。
356
+ */
357
+ declare function timingSafeEqualString(a: string, b: string): boolean;
358
+
359
+ export { OAuthError, type OidcDiscovery, SsoClient, type SsoClientConfig, SsoError, type SsoErrorCode, type SsoUser, type TokenData, type TokenResponse, type TokenStorage, clearAllSsoData, clearVerifiersForClients, createSecureStorage, generateCodeChallenge, generateCodeVerifier, generateState, getOAuthState, getPkceVerifier, getReturnUrl, getTokenData, getTokenStorage, isTrustedReturnUrl, mapOAuthErrorToSsoCode, removeOAuthState, removePkceVerifier, removeReturnUrl, removeTokenData, saveOAuthState, savePkceVerifier, saveReturnUrl, saveTokenData, setTokenStorage, timingSafeEqualString };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * Token 存储抽象层
3
3
  *
4
- * 默认使用 localStorage,以支持多 Tab 间自动同步 token 并避免并发刷新。
5
- * 对 XSS 敏感的子项目可通过 setTokenStorage() 注入更安全的自定义实现
6
- *(如内存存储、Service Worker 封装、或带加密的 storage)。
4
+ * 分两类存储:
5
+ * - Token 数据:默认使用内存存储,Public Client 浏览器中 refresh_token 不落盘,
6
+ * XSS 无法窃取长期凭证。对需要多 Tab 共享 token 或 BFF/Confidential Client 场景,
7
+ * 可通过 setTokenStorage() 注入 localStorage 实现(如 createSecureStorage({ persist: true }))。
8
+ * - 临时数据(PKCE verifier / state / returnUrl / popup nonce):必须跨整页重定向存活
9
+ * (login() 会 302 跳转到 SSO 中心再回来),因此默认写入 sessionStorage;
10
+ * SSR 等无 sessionStorage 环境自动降级为内存 Map。
7
11
  *
8
12
  * 多 client 隔离:
9
13
  * - token / state / return_url 均支持按 clientId 隔离 key
@@ -27,6 +31,19 @@ interface TokenStorage {
27
31
  set(key: string, value: string): void;
28
32
  remove(key: string): void;
29
33
  }
34
+ /**
35
+ * 创建存储实现
36
+ *
37
+ * @param options.persist 是否持久化到 localStorage。默认 false(内存存储)。
38
+ * ⚠️ 安全警告:persist=true 会将 refresh_token 明文写入 localStorage,
39
+ * 任何 XSS 均可在不被检测的情况下读取。仅在 BFF/Confidential Client
40
+ * 且 refresh_token 不直接暴露给浏览器的场景下使用。
41
+ * - Confidential/BFF 子项目可设为 true。
42
+ * - Public Client 在浏览器中应保持 false(默认)。
43
+ */
44
+ declare function createSecureStorage(options?: {
45
+ persist?: boolean;
46
+ }): TokenStorage;
30
47
  /**
31
48
  * 设置自定义存储实现
32
49
  */
@@ -85,6 +102,8 @@ interface SsoClientConfig {
85
102
  ssoBaseUrl: string;
86
103
  /** 请求的 scope(空格分隔),如 "openid profile phone" */
87
104
  scopes?: string;
105
+ /** RP-Initiated Logout 返回地址(可选)。不传时回退到 redirectUri */
106
+ postLogoutRedirectUri?: string;
88
107
  }
89
108
  /** 用户信息 */
90
109
  interface SsoUser {
@@ -141,13 +160,19 @@ declare class SsoClient {
141
160
  private _getTokenEndpoint;
142
161
  /** 获取 userinfo 端点 URL(优先 Discovery,回退默认) */
143
162
  private _getUserinfoEndpoint;
163
+ /**
164
+ * 开放重定向防护:仅保存相对路径或与当前页面同源的 returnUrl,
165
+ * 其余(如 https://evil.com)忽略并告警,防止回调后跳转到钓鱼站点。
166
+ */
167
+ private _saveReturnUrlIfTrusted;
144
168
  /**
145
169
  * 发起 SSO 登录
146
170
  *
147
171
  * 生成 PKCE code_verifier/code_challenge 和 state 参数,
148
172
  * 构建 authorize URL,通过 302 跳转到 SSO 登录页。
149
173
  *
150
- * @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage)
174
+ * @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage;
175
+ * 仅允许相对路径或同源绝对 URL,否则忽略并告警)
151
176
  */
152
177
  login(returnUrl?: string): Promise<void>;
153
178
  /**
@@ -156,11 +181,49 @@ declare class SsoClient {
156
181
  * 适用于需要手动处理跳转的场景。
157
182
  */
158
183
  getLoginUrl(returnUrl?: string): Promise<string>;
184
+ /**
185
+ * 弹窗模式 SSO 登录
186
+ *
187
+ * 打开一个小窗口进行登录,认证完成后窗口自动关闭,
188
+ * 主页面不丢失状态(适用于 SPA 中需要保持表单/浏览上下文的场景)。
189
+ *
190
+ * 流程:
191
+ * 1. window.open() 打开授权 URL 到弹窗
192
+ * 2. 用户在弹窗中完成登录
193
+ * 3. 弹窗加载 CallbackPage 时检测到 window.opener,通过 postMessage 回传回调 URL
194
+ * 4. 主页面收到消息后调用 handleCallback() 交换 token
195
+ * 5. 弹窗自动关闭
196
+ *
197
+ * @param options - 弹窗配置
198
+ * @param options.returnUrl - 登录成功后的返回地址
199
+ * @param options.width - 弹窗宽度(默认 480)
200
+ * @param options.height - 弹窗高度(默认 640)
201
+ * @returns TokenData
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * const client = new SsoClient({ clientId: "xxx", redirectUri: "https://myapp.com/callback", ssoBaseUrl: "https://nihplod.cn" });
206
+ * try {
207
+ * const tokenData = await client.loginPopup({ returnUrl: "/dashboard" });
208
+ * console.log("登录成功", tokenData);
209
+ * } catch (err) {
210
+ * if (err instanceof SsoError && err.code === "popup_blocked") {
211
+ * // 弹窗被拦截,回退到同页重定向
212
+ * await client.login("/dashboard");
213
+ * }
214
+ * }
215
+ * ```
216
+ */
217
+ loginPopup(options?: {
218
+ returnUrl?: string;
219
+ width?: number;
220
+ height?: number;
221
+ }): Promise<TokenData>;
159
222
  /**
160
223
  * 处理 OAuth 回调
161
224
  *
162
225
  * 解析回调 URL,校验 state 参数,用授权码交换 token。
163
- * 成功后 token 自动保存到 sessionStorage。
226
+ * 成功后 token 自动保存到 token 存储(默认内存,可通过 setTokenStorage 定制)。
164
227
  *
165
228
  * @param callbackUrl - 完整的回调 URL(window.location.href)
166
229
  * @returns TokenData 或 null
@@ -247,9 +310,16 @@ declare function generateState(): string;
247
310
  * SSO SDK 错误类型
248
311
  */
249
312
  /** SSO 错误码 */
250
- type SsoErrorCode = "invalid_config" | "state_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "sso_server_error" | "network_error" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch";
251
- /** OAuth 2.0 标准错误码 */
252
- type OAuthErrorCode = "invalid_request" | "invalid_client" | "invalid_grant" | "unauthorized_client" | "unsupported_grant_type" | "invalid_scope" | "access_denied" | "server_error" | "rate_limited";
313
+ type SsoErrorCode = "invalid_config" | "state_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch";
314
+ /**
315
+ * 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode
316
+ *
317
+ * @param oauthError 服务端返回的 error 字段
318
+ * @param context 调用上下文:"token_exchange"(授权码换 token)或 "refresh"(刷新)。
319
+ * invalid_grant 在换 token 场景多为授权码过期/已用,在刷新场景多为会话失效,
320
+ * 需按上下文细化映射。
321
+ */
322
+ declare function mapOAuthErrorToSsoCode(oauthError: string, context?: "token_exchange" | "refresh"): SsoErrorCode;
253
323
  /**
254
324
  * SSO SDK 自定义错误
255
325
  */
@@ -260,13 +330,30 @@ declare class SsoError extends Error {
260
330
  constructor(code: SsoErrorCode, description: string, cause?: unknown);
261
331
  }
262
332
  /**
263
- * OAuth 2.0 服务端返回的错误
333
+ * OAuth 2.0 协议层错误(RFC 6749 §5.2)
334
+ *
335
+ * 用于表示服务端原样返回的 OAuth 错误(error / error_description / error_uri),
336
+ * 与 SDK 语义的 SsoError 区分。
264
337
  */
265
338
  declare class OAuthError extends Error {
266
- readonly code: OAuthErrorCode;
339
+ readonly code: string;
267
340
  readonly description: string;
268
341
  readonly uri?: string;
269
- constructor(code: OAuthErrorCode, description: string, uri?: string);
342
+ constructor(code: string, description: string, uri?: string);
270
343
  }
271
344
 
272
- export { OAuthError, type OAuthErrorCode, type OidcDiscovery, SsoClient, type SsoClientConfig, SsoError, type SsoErrorCode, type SsoUser, type TokenData, type TokenResponse, type TokenStorage, clearAllSsoData, clearVerifiersForClients, generateCodeChallenge, generateCodeVerifier, generateState, getOAuthState, getPkceVerifier, getReturnUrl, getTokenData, getTokenStorage, removeOAuthState, removePkceVerifier, removeReturnUrl, removeTokenData, saveOAuthState, savePkceVerifier, saveReturnUrl, saveTokenData, setTokenStorage };
345
+ /**
346
+ * 安全相关小工具:returnUrl 开放重定向校验、常量时间字符串比较
347
+ */
348
+ /**
349
+ * 校验 returnUrl 是否可信(防开放重定向)。
350
+ * 仅允许:相对路径(且不以 // 开头)或与 currentOrigin 完全同源的绝对 URL。
351
+ */
352
+ declare function isTrustedReturnUrl(url: string, currentOrigin: string): boolean;
353
+ /**
354
+ * 常量时间字符串比较(避免 state / at_hash / nonce 等机密值的时序侧信道)。
355
+ * 长度不同也执行完整循环,不提前返回。
356
+ */
357
+ declare function timingSafeEqualString(a: string, b: string): boolean;
358
+
359
+ export { OAuthError, type OidcDiscovery, SsoClient, type SsoClientConfig, SsoError, type SsoErrorCode, type SsoUser, type TokenData, type TokenResponse, type TokenStorage, clearAllSsoData, clearVerifiersForClients, createSecureStorage, generateCodeChallenge, generateCodeVerifier, generateState, getOAuthState, getPkceVerifier, getReturnUrl, getTokenData, getTokenStorage, isTrustedReturnUrl, mapOAuthErrorToSsoCode, removeOAuthState, removePkceVerifier, removeReturnUrl, removeTokenData, saveOAuthState, savePkceVerifier, saveReturnUrl, saveTokenData, setTokenStorage, timingSafeEqualString };