@nihplod/sso-sdk 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +358 -360
  2. package/package.json +3 -2
package/README.md CHANGED
@@ -1,360 +1,358 @@
1
- # @nihplod/sso-sdk
2
-
3
- NIHPLOD SSO 子项目接入 SDK — OAuth 2.0 授权码 + PKCE 客户端封装。
4
-
5
- ## 安装
6
-
7
- ```bash
8
- npm install @nihplod/sso-sdk
9
- ```
10
-
11
- ## 快速开始
12
-
13
- ```typescript
14
- import { SsoClient } from "@nihplod/sso-sdk";
15
-
16
- const sso = new SsoClient({
17
- clientId: "your-client-id",
18
- redirectUri: "https://yourapp.com/callback",
19
- ssoBaseUrl: "https://nihplod.cn",
20
- });
21
-
22
- // 1. 发起登录
23
- await sso.login();
24
-
25
- // 2. 处理回调(在回调页面中)
26
- const token = await sso.handleCallback(window.location.href);
27
-
28
- // 3. 获取用户信息
29
- const user = await sso.getUserInfo();
30
-
31
- // 4. 登出
32
- await sso.logout();
33
- ```
34
-
35
- ---
36
-
37
- ## API 参考
38
-
39
- ### `new SsoClient(config)`
40
-
41
- 创建 SSO 客户端实例。
42
-
43
- | 参数 | 类型 | 必填 | 说明 |
44
- |------|------|------|------|
45
- | `clientId` | `string` | ✅ | OAuth Client ID |
46
- | `redirectUri` | `string` | ✅ | 回调 URL,须与注册时一致 |
47
- | `ssoBaseUrl` | `string` | ✅ | SSO 中心地址,如 `https://nihplod.cn` |
48
- | `scopes` | `string` | ❌ | 空格分隔的 scope,默认 `"openid profile"` |
49
- | `clientSecret` | `string` | ❌ | **仅 Confidential Client 需要**。浏览器端 SPA(Public Client)请勿传入,避免密钥泄露。BFF / Next.js Route Handler 可传入 |
50
-
51
- ### `sso.login(returnUrl?)`
52
-
53
- 发起 SSO 登录。生成 PKCE 参数后 302 跳转到 SSO 登录页。
54
-
55
- | 参数 | 类型 | 说明 |
56
- |------|------|------|
57
- | `returnUrl` | `string` | 登录成功后的返回地址(可选) |
58
-
59
- ### `sso.getLoginUrl(returnUrl?)`
60
-
61
- 构建登录 URL 字符串,不执行跳转。返回 `Promise<string>`。
62
-
63
- ### `sso.handleCallback(callbackUrl)`
64
-
65
- 处理 OAuth 回调。解析 URL 中的 `code` 和 `state`,校验 state 后用 code 换取 token。
66
-
67
- | 参数 | 类型 | 说明 |
68
- |------|------|------|
69
- | `callbackUrl` | `string` | 完整的回调 URL(`window.location.href`) |
70
-
71
- 返回 `Promise<TokenData>`。
72
-
73
- ### `sso.refreshToken()`
74
-
75
- 使用 refresh_token 刷新 access_token。采用互斥锁防止并发刷新。
76
-
77
- 返回 `Promise<TokenData>`。
78
-
79
- ### `sso.getUserInfo()`
80
-
81
- 获取当前用户信息。若 access_token 已过期则自动刷新。
82
-
83
- 返回 `Promise<SsoUser>`。
84
-
85
- ```typescript
86
- interface SsoUser {
87
- sub: string;
88
- nickname?: string;
89
- avatar?: string;
90
- phone?: string; // 脱敏手机号
91
- membership_level?: string;
92
- total_points?: number;
93
- }
94
- ```
95
-
96
- ### `sso.getAccessToken()`
97
-
98
- 获取当前有效的 access_token。若已过期则自动刷新。
99
-
100
- 返回 `Promise<string | null>`。
101
-
102
- ### `sso.isAuthenticated()`
103
-
104
- 检查是否已认证(仅本地检查,不发起网络请求)。
105
-
106
- 返回 `boolean`。
107
-
108
- ### `sso.logout(redirectToSso?)`
109
-
110
- 清除本地 token 数据,并尝试撤销服务端 refresh_token。
111
-
112
- | 参数 | 类型 | 默认 | 说明 |
113
- |------|------|------|------|
114
- | `redirectToSso` | `boolean` | `false` | 是否重定向到 SSO 登出页(OIDC RP-Initiated Logout) |
115
-
116
- 当 `redirectToSso=true` 时,会跳转到 `/logout?client_id=...&post_logout_redirect_uri=...`,
117
- 由主站完成会话清除后回到子项目回调地址。
118
-
119
- ### `sso.getDiscovery()`
120
-
121
- 获取 OIDC Discovery 文档。
122
-
123
- 返回 `Promise<OidcDiscovery>`。
124
-
125
- ---
126
-
127
- ## TypeScript 类型
128
-
129
- ```typescript
130
- // SsoClient 配置
131
- interface SsoClientConfig {
132
- clientId: string;
133
- redirectUri: string;
134
- ssoBaseUrl: string;
135
- scopes?: string;
136
- clientSecret?: string; // 仅 Confidential Client 需要
137
- }
138
-
139
- // Token 数据
140
- interface TokenData {
141
- access_token: string;
142
- token_type: string;
143
- expires_in: number;
144
- refresh_token: string;
145
- id_token?: string;
146
- issued_at: number;
147
- expires_at: number;
148
- }
149
-
150
- // 用户信息
151
- interface SsoUser {
152
- sub: string;
153
- nickname?: string;
154
- avatar?: string;
155
- phone?: string;
156
- membership_level?: string;
157
- total_points?: number;
158
- }
159
-
160
- // OIDC Discovery
161
- interface OidcDiscovery {
162
- issuer: string;
163
- authorization_endpoint: string;
164
- token_endpoint: string;
165
- userinfo_endpoint: string;
166
- jwks_uri: string;
167
- introspection_endpoint: string;
168
- revocation_endpoint?: string;
169
- end_session_endpoint?: string;
170
- scopes_supported: string[];
171
- response_types_supported: string[];
172
- grant_types_supported: string[];
173
- code_challenge_methods_supported: string[];
174
- }
175
- ```
176
-
177
- ---
178
-
179
- ## React 绑定
180
-
181
- ### `<SsoProvider>`
182
-
183
- ```tsx
184
- import { SsoProvider } from "@nihplod/sso-sdk/react";
185
-
186
- <SsoProvider
187
- config={{
188
- clientId: "...",
189
- redirectUri: "...",
190
- ssoBaseUrl: "...",
191
- scopes: "openid profile",
192
- }}
193
- refreshThreshold={60} // 过期前 60s 自动刷新
194
- >
195
- <App />
196
- </SsoProvider>
197
- ```
198
-
199
- ### `useSso()`
200
-
201
- ```tsx
202
- import { useSso } from "@nihplod/sso-sdk/react";
203
-
204
- const {
205
- user, // SsoUser | null
206
- isAuthenticated, // boolean
207
- isLoading, // boolean
208
- login, // (returnUrl?: string) => Promise<void>
209
- logout, // (redirectToSso?: boolean) => Promise<void>
210
- refreshUser, // () => Promise<void>
211
- getAccessToken, // () => Promise<string | null>
212
- client, // SsoClient 实例
213
- } = useSso();
214
- ```
215
-
216
- ### `<RequireAuth>`
217
-
218
- ```tsx
219
- import { RequireAuth } from "@nihplod/sso-sdk/react";
220
-
221
- <RequireAuth>
222
- <ProtectedContent />
223
- </RequireAuth>
224
- ```
225
-
226
- ### `withAuth(Component)`
227
-
228
- ```tsx
229
- import { withAuth } from "@nihplod/sso-sdk/react";
230
-
231
- function DashboardPage() { return <div>Dashboard</div>; }
232
- export default withAuth(DashboardPage);
233
- ```
234
-
235
- ### `<CallbackPage>`
236
-
237
- ```tsx
238
- import { CallbackPage } from "@nihplod/sso-sdk/react";
239
-
240
- // 在回调路由中渲染此组件即可
241
- export default function AuthCallback() {
242
- return <CallbackPage />;
243
- }
244
- ```
245
-
246
- ---
247
-
248
- ## Next.js 绑定
249
-
250
- Next.js 推荐采用 **Middleware + Route Handler** 的 BFF 模式,token 存放在
251
- `httpOnly` Cookie 中,JavaScript 无法读取,安全性最高。
252
-
253
- ```typescript
254
- // src/middleware.ts
255
- import { createSsoMiddleware } from "@nihplod/sso-sdk/next";
256
-
257
- export const middleware = createSsoMiddleware({
258
- clientId: "...",
259
- ssoBaseUrl: "https://nihplod.cn",
260
- redirectUri: "https://yourapp.com/api/auth/callback",
261
- scopes: "openid profile",
262
- publicPaths: ["/", "/public"],
263
- // Confidential Client(BFF)可传入 clientSecret
264
- // clientSecret: process.env.SSO_CLIENT_SECRET,
265
- });
266
-
267
- export const config = {
268
- matcher: ["/((?!_next|favicon.ico).*)"],
269
- };
270
- ```
271
-
272
- ```typescript
273
- // src/app/api/auth/callback/route.ts
274
- import { createCallbackRouteHandler } from "@nihplod/sso-sdk/next";
275
-
276
- export const GET = createCallbackRouteHandler({
277
- clientId: "...",
278
- ssoBaseUrl: "https://nihplod.cn",
279
- redirectUri: "https://yourapp.com/api/auth/callback",
280
- defaultReturnPath: "/dashboard",
281
- // 与 middleware 一致,Confidential Client 可传入 clientSecret
282
- });
283
- ```
284
-
285
- ```typescript
286
- // src/app/api/auth/logout/route.ts
287
- import { createLogoutRouteHandler } from "@nihplod/sso-sdk/next";
288
-
289
- export const GET = createLogoutRouteHandler({
290
- clientId: "...",
291
- ssoBaseUrl: "https://nihplod.cn",
292
- redirectUri: "https://yourapp.com/api/auth/callback",
293
- postLogoutRedirectUri: "https://yourapp.com/",
294
- redirectToSso: true,
295
- });
296
- ```
297
-
298
- 页面中使用标准 `<a>` 跳转登出端点即可:
299
-
300
- ```tsx
301
- <a href="/api/auth/logout">退出登录</a>
302
- ```
303
-
304
- ### Cookie 配置
305
-
306
- 默认 Cookie 名称:
307
-
308
- | Cookie | 默认名称 | 说明 |
309
- |--------|----------|------|
310
- | access_token | `__Host-nihplod_sso_at` | 要求 Secure + Path=/ + 无 Domain |
311
- | refresh_token | `__Host-nihplod_sso_rt` | 要求 Secure + Path=/ + 无 Domain |
312
- | state | `__Host-nihplod_sso_state` | 要求 Secure + Path=/ + 无 Domain |
313
- | return_url | `__Host-nihplod_sso_return` | 要求 Secure + Path=/ + 无 Domain |
314
- | verifier | `__Secure-nihplod_sso_verifier` | 要求 Secure + 无 Domain;Path 为回调路径,因此使用 `__Secure-` 前缀 |
315
-
316
- > 本地开发若使用 `http://localhost`,浏览器会拒绝 `Secure` Cookie。此时可仅在本地关闭 `secure`,生产环境必须启用 HTTPS。
317
-
318
- ---
319
-
320
- ## 安全建议与 Token 存储
321
-
322
- SDK 默认将 token 保存在**内存**中,Public Client(SPA/移动端/桌面端)在浏览器中不应把 `refresh_token` 写入 `localStorage`,以防止 XSS 窃取长期凭证。
323
-
324
- 如果子项目是 **Next.js BFF / Confidential Client**,可以将 token 保存在 `localStorage` 以便多 Tab 共享:
325
-
326
- ```typescript
327
- import { setTokenStorage, createSecureStorage } from "@nihplod/sso-sdk";
328
-
329
- setTokenStorage(createSecureStorage({ persist: true }));
330
- ```
331
-
332
- 生产环境更推荐通过 Service Worker 或 HTTP-only Cookie 封装 refresh token,前端只持有 access token 短期凭证。
333
-
334
- ---
335
-
336
- ## 工具函数
337
-
338
- ```typescript
339
- import {
340
- generateCodeVerifier,
341
- generateCodeChallenge,
342
- generateState,
343
- setTokenStorage,
344
- createSecureStorage,
345
- getTokenData,
346
- saveTokenData,
347
- removeTokenData,
348
- clearAllSsoData,
349
- } from "@nihplod/sso-sdk";
350
-
351
- // PKCE
352
- const verifier = generateCodeVerifier(64);
353
- const challenge = await generateCodeChallenge(verifier);
354
-
355
- // State
356
- const state = generateState();
357
-
358
- // 自定义 Token 存储(默认内存存储)
359
- setTokenStorage(createSecureStorage({ persist: false }));
360
- ```
1
+ # @nihplod/sso-sdk
2
+
3
+ NIHPLOD SSO client SDK — OAuth 2.0 Authorization Code + PKCE wrapper for sub-projects.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @nihplod/sso-sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { SsoClient } from "@nihplod/sso-sdk";
15
+
16
+ const sso = new SsoClient({
17
+ clientId: "your-client-id",
18
+ redirectUri: "https://yourapp.com/callback",
19
+ ssoBaseUrl: "https://nihplod.cn",
20
+ });
21
+
22
+ // 1. Initiate login
23
+ await sso.login();
24
+
25
+ // 2. Handle callback (inside the callback page)
26
+ const token = await sso.handleCallback(window.location.href);
27
+
28
+ // 3. Get user info
29
+ const user = await sso.getUserInfo();
30
+
31
+ // 4. Logout
32
+ await sso.logout();
33
+ ```
34
+
35
+ ---
36
+
37
+ ## API Reference
38
+
39
+ ### `new SsoClient(config)`
40
+
41
+ Create an SSO client instance.
42
+
43
+ | Parameter | Type | Required | Description |
44
+ |------|------|------|------|
45
+ | `clientId` | `string` | ✅ | OAuth Client ID |
46
+ | `redirectUri` | `string` | ✅ | Callback URL, must match the one registered |
47
+ | `ssoBaseUrl` | `string` | ✅ | SSO provider base URL, e.g. `https://nihplod.cn` |
48
+ | `scopes` | `string` | ❌ | Space-separated scopes, default `"openid profile"` |
49
+ | `clientSecret` | `string` | ❌ | **Only for Confidential Clients**. Do NOT pass this in browser SPA (Public Client) to avoid leaking secrets. BFF / Next.js Route Handlers may pass it. |
50
+
51
+ ### `sso.login(returnUrl?)`
52
+
53
+ Initiate SSO login. Generates PKCE parameters and redirects to the SSO login page.
54
+
55
+ | Parameter | Type | Description |
56
+ |------|------|------|
57
+ | `returnUrl` | `string` | Optional URL to return to after login |
58
+
59
+ ### `sso.getLoginUrl(returnUrl?)`
60
+
61
+ Build the login URL string without redirecting. Returns `Promise<string>`.
62
+
63
+ ### `sso.handleCallback(callbackUrl)`
64
+
65
+ Handle the OAuth callback. Parses `code` and `state` from the URL, validates state, and exchanges the code for tokens.
66
+
67
+ | Parameter | Type | Description |
68
+ |------|------|------|
69
+ | `callbackUrl` | `string` | Full callback URL (`window.location.href`) |
70
+
71
+ Returns `Promise<TokenData>`.
72
+
73
+ ### `sso.refreshToken()`
74
+
75
+ Refresh the access_token using the refresh_token. Uses a mutex to prevent concurrent refresh requests.
76
+
77
+ Returns `Promise<TokenData>`.
78
+
79
+ ### `sso.getUserInfo()`
80
+
81
+ Fetch current user info. Automatically refreshes the access_token if expired.
82
+
83
+ Returns `Promise<SsoUser>`.
84
+
85
+ ```typescript
86
+ interface SsoUser {
87
+ sub: string;
88
+ nickname?: string;
89
+ avatar?: string;
90
+ phone?: string; // Masked phone number
91
+ membership_level?: string;
92
+ total_points?: number;
93
+ }
94
+ ```
95
+
96
+ ### `sso.getAccessToken()`
97
+
98
+ Get the current valid access_token. Automatically refreshes if expired.
99
+
100
+ Returns `Promise<string | null>`.
101
+
102
+ ### `sso.isAuthenticated()`
103
+
104
+ Check whether the user is authenticated (local check only, no network request).
105
+
106
+ Returns `boolean`.
107
+
108
+ ### `sso.logout(redirectToSso?)`
109
+
110
+ Clear local token data and attempt to revoke the server-side refresh_token.
111
+
112
+ | Parameter | Type | Default | Description |
113
+ |------|------|------|------|
114
+ | `redirectToSso` | `boolean` | `false` | Whether to redirect to the SSO logout page (OIDC RP-Initiated Logout) |
115
+
116
+ When `redirectToSso=true`, the user is redirected to `/logout?client_id=...&post_logout_redirect_uri=...`. The main site clears the session and then returns to the sub-project callback address.
117
+
118
+ ### `sso.getDiscovery()`
119
+
120
+ Fetch the OIDC Discovery document.
121
+
122
+ Returns `Promise<OidcDiscovery>`.
123
+
124
+ ---
125
+
126
+ ## TypeScript Types
127
+
128
+ ```typescript
129
+ // SsoClient config
130
+ interface SsoClientConfig {
131
+ clientId: string;
132
+ redirectUri: string;
133
+ ssoBaseUrl: string;
134
+ scopes?: string;
135
+ clientSecret?: string; // Only for Confidential Clients
136
+ }
137
+
138
+ // Token data
139
+ interface TokenData {
140
+ access_token: string;
141
+ token_type: string;
142
+ expires_in: number;
143
+ refresh_token: string;
144
+ id_token?: string;
145
+ issued_at: number;
146
+ expires_at: number;
147
+ }
148
+
149
+ // User info
150
+ interface SsoUser {
151
+ sub: string;
152
+ nickname?: string;
153
+ avatar?: string;
154
+ phone?: string;
155
+ membership_level?: string;
156
+ total_points?: number;
157
+ }
158
+
159
+ // OIDC Discovery
160
+ interface OidcDiscovery {
161
+ issuer: string;
162
+ authorization_endpoint: string;
163
+ token_endpoint: string;
164
+ userinfo_endpoint: string;
165
+ jwks_uri: string;
166
+ introspection_endpoint: string;
167
+ revocation_endpoint?: string;
168
+ end_session_endpoint?: string;
169
+ scopes_supported: string[];
170
+ response_types_supported: string[];
171
+ grant_types_supported: string[];
172
+ code_challenge_methods_supported: string[];
173
+ }
174
+ ```
175
+
176
+ ---
177
+
178
+ ## React Bindings
179
+
180
+ ### `<SsoProvider>`
181
+
182
+ ```tsx
183
+ import { SsoProvider } from "@nihplod/sso-sdk/react";
184
+
185
+ <SsoProvider
186
+ config={{
187
+ clientId: "...",
188
+ redirectUri: "...",
189
+ ssoBaseUrl: "...",
190
+ scopes: "openid profile",
191
+ }}
192
+ refreshThreshold={60} // Auto-refresh 60s before expiry
193
+ >
194
+ <App />
195
+ </SsoProvider>
196
+ ```
197
+
198
+ ### `useSso()`
199
+
200
+ ```tsx
201
+ import { useSso } from "@nihplod/sso-sdk/react";
202
+
203
+ const {
204
+ user, // SsoUser | null
205
+ isAuthenticated, // boolean
206
+ isLoading, // boolean
207
+ login, // (returnUrl?: string) => Promise<void>
208
+ logout, // (redirectToSso?: boolean) => Promise<void>
209
+ refreshUser, // () => Promise<void>
210
+ getAccessToken, // () => Promise<string | null>
211
+ client, // SsoClient instance
212
+ } = useSso();
213
+ ```
214
+
215
+ ### `<RequireAuth>`
216
+
217
+ ```tsx
218
+ import { RequireAuth } from "@nihplod/sso-sdk/react";
219
+
220
+ <RequireAuth>
221
+ <ProtectedContent />
222
+ </RequireAuth>
223
+ ```
224
+
225
+ ### `withAuth(Component)`
226
+
227
+ ```tsx
228
+ import { withAuth } from "@nihplod/sso-sdk/react";
229
+
230
+ function DashboardPage() { return <div>Dashboard</div>; }
231
+ export default withAuth(DashboardPage);
232
+ ```
233
+
234
+ ### `<CallbackPage>`
235
+
236
+ ```tsx
237
+ import { CallbackPage } from "@nihplod/sso-sdk/react";
238
+
239
+ // Render this component in the callback route
240
+ export default function AuthCallback() {
241
+ return <CallbackPage />;
242
+ }
243
+ ```
244
+
245
+ ---
246
+
247
+ ## Next.js Bindings
248
+
249
+ For Next.js, the recommended approach is **Middleware + Route Handler** BFF pattern. Tokens are stored in `httpOnly` cookies so JavaScript cannot read them, providing the highest security.
250
+
251
+ ```typescript
252
+ // src/middleware.ts
253
+ import { createSsoMiddleware } from "@nihplod/sso-sdk/next";
254
+
255
+ export const middleware = createSsoMiddleware({
256
+ clientId: "...",
257
+ ssoBaseUrl: "https://nihplod.cn",
258
+ redirectUri: "https://yourapp.com/api/auth/callback",
259
+ scopes: "openid profile",
260
+ publicPaths: ["/", "/public"],
261
+ // Confidential Client (BFF) can pass clientSecret
262
+ // clientSecret: process.env.SSO_CLIENT_SECRET,
263
+ });
264
+
265
+ export const config = {
266
+ matcher: ["/((?!_next|favicon.ico).*)"],
267
+ };
268
+ ```
269
+
270
+ ```typescript
271
+ // src/app/api/auth/callback/route.ts
272
+ import { createCallbackRouteHandler } from "@nihplod/sso-sdk/next";
273
+
274
+ export const GET = createCallbackRouteHandler({
275
+ clientId: "...",
276
+ ssoBaseUrl: "https://nihplod.cn",
277
+ redirectUri: "https://yourapp.com/api/auth/callback",
278
+ defaultReturnPath: "/dashboard",
279
+ // Same as middleware; Confidential Client can pass clientSecret
280
+ });
281
+ ```
282
+
283
+ ```typescript
284
+ // src/app/api/auth/logout/route.ts
285
+ import { createLogoutRouteHandler } from "@nihplod/sso-sdk/next";
286
+
287
+ export const GET = createLogoutRouteHandler({
288
+ clientId: "...",
289
+ ssoBaseUrl: "https://nihplod.cn",
290
+ redirectUri: "https://yourapp.com/api/auth/callback",
291
+ postLogoutRedirectUri: "https://yourapp.com/",
292
+ redirectToSso: true,
293
+ });
294
+ ```
295
+
296
+ Use a standard `<a>` tag to trigger the logout endpoint:
297
+
298
+ ```tsx
299
+ <a href="/api/auth/logout">Logout</a>
300
+ ```
301
+
302
+ ### Cookie Configuration
303
+
304
+ Default cookie names:
305
+
306
+ | Cookie | Default Name | Description |
307
+ |--------|--------------|-------------|
308
+ | access_token | `__Host-nihplod_sso_at` | Requires Secure + Path=/ + no Domain |
309
+ | refresh_token | `__Host-nihplod_sso_rt` | Requires Secure + Path=/ + no Domain |
310
+ | state | `__Host-nihplod_sso_state` | Requires Secure + Path=/ + no Domain |
311
+ | return_url | `__Host-nihplod_sso_return` | Requires Secure + Path=/ + no Domain |
312
+ | verifier | `__Secure-nihplod_sso_verifier` | Requires Secure + no Domain; Path is the callback path, therefore uses `__Secure-` prefix |
313
+
314
+ > For local development with `http://localhost`, the browser will reject `Secure` cookies. You may disable `secure` locally, but HTTPS is mandatory in production.
315
+
316
+ ---
317
+
318
+ ## Security Recommendations and Token Storage
319
+
320
+ 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
+
322
+ If the sub-project is a **Next.js BFF / Confidential Client**, you can store tokens in `localStorage` for multi-tab sharing:
323
+
324
+ ```typescript
325
+ import { setTokenStorage, createSecureStorage } from "@nihplod/sso-sdk";
326
+
327
+ setTokenStorage(createSecureStorage({ persist: true }));
328
+ ```
329
+
330
+ In production, it is more secure to keep the refresh token in a Service Worker or HTTP-only cookie, exposing only the short-lived access token to the frontend.
331
+
332
+ ---
333
+
334
+ ## Utility Functions
335
+
336
+ ```typescript
337
+ import {
338
+ generateCodeVerifier,
339
+ generateCodeChallenge,
340
+ generateState,
341
+ setTokenStorage,
342
+ createSecureStorage,
343
+ getTokenData,
344
+ saveTokenData,
345
+ removeTokenData,
346
+ clearAllSsoData,
347
+ } from "@nihplod/sso-sdk";
348
+
349
+ // PKCE
350
+ const verifier = generateCodeVerifier(64);
351
+ const challenge = await generateCodeChallenge(verifier);
352
+
353
+ // State
354
+ const state = generateState();
355
+
356
+ // Custom token storage (default is memory storage)
357
+ setTokenStorage(createSecureStorage({ persist: false }));
358
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nihplod/sso-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "NIHPLOD SSO 子项目接入 SDK — OAuth 2.0 授权码 + PKCE 客户端封装",
5
5
  "license": "UNLICENSED",
6
6
  "main": "./dist/index.js",
@@ -32,7 +32,8 @@
32
32
  "dev": "tsup --watch",
33
33
  "test": "vitest run",
34
34
  "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
35
- "typecheck": "tsc --noEmit"
35
+ "typecheck": "tsc --noEmit",
36
+ "prepublishOnly": "npm run build"
36
37
  },
37
38
  "peerDependencies": {
38
39
  "next": ">=14",