@xfe-repo/cli-plugin-oauth 2.0.8 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @xfe-repo/cli-plugin-oauth
2
2
 
3
+ ## 2.0.9
4
+
5
+ ### Patch Changes
6
+
7
+ - 优化回调
8
+
3
9
  ## 2.0.8
4
10
 
5
11
  ### Patch Changes
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @xfe-repo/cli-plugin-oauth - OAuth 回调服务
3
3
  *
4
- * 基于 Hono 启动临时本地 HTTP 服务,等待 OAuth 授权服务器回跳 redirectUri。
4
+ * 基于 Node HTTP 启动临时本地服务,等待 OAuth 授权服务器回跳 redirectUri。
5
5
  */
6
6
  export interface OAuthCallbackServer {
7
7
  waitForCallback(): Promise<URL>;
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * @xfe-repo/cli-plugin-oauth - OAuth 回调服务
3
3
  *
4
- * 基于 Hono 启动临时本地 HTTP 服务,等待 OAuth 授权服务器回跳 redirectUri。
4
+ * 基于 Node HTTP 启动临时本地服务,等待 OAuth 授权服务器回跳 redirectUri。
5
5
  */
6
- import { serve } from '@hono/node-server';
7
- import { Hono } from 'hono';
6
+ import { createServer } from 'node:http';
8
7
  // ─── Constants ─────────────────────────────────────────────
9
8
  const CALLBACK_TIMEOUT_MS = 300000;
10
9
  const HTTP_OK = 200;
11
10
  const HTTP_BAD_REQUEST = 400;
12
11
  const HTTP_NOT_FOUND = 404;
12
+ const HTML_CONTENT_TYPE = 'text/html; charset=utf-8';
13
13
  const SUCCESS_HTML = '<!doctype html><html lang="zh"><body>OAuth 登录完成,可以关闭此窗口。</body></html>';
14
14
  const FAILURE_HTML = '<!doctype html><html lang="zh"><body>OAuth 登录失败,请回到终端查看错误。</body></html>';
15
15
  // ─── Public API ────────────────────────────────────────────
@@ -32,23 +32,31 @@ export async function startOAuthCallbackServer(redirectUri) {
32
32
  resolveCallback = resolve;
33
33
  rejectCallback = reject;
34
34
  });
35
- const app = new Hono();
36
- app.get(redirectUrl.pathname, (ctx) => {
37
- const currentUrl = new URL(ctx.req.url);
35
+ const httpServer = createServer((request, response) => {
36
+ const currentUrl = new URL(request.url ?? '/', redirectUrl.origin);
37
+ if (currentUrl.pathname !== redirectUrl.pathname) {
38
+ writeHtml(response, HTTP_NOT_FOUND, FAILURE_HTML);
39
+ return;
40
+ }
38
41
  const error = currentUrl.searchParams.get('error');
39
42
  if (error) {
40
43
  const description = currentUrl.searchParams.get('error_description');
41
44
  rejectCallback(new Error(description ? `${error}: ${description}` : error));
42
- return ctx.html(FAILURE_HTML, HTTP_BAD_REQUEST);
45
+ writeHtml(response, HTTP_BAD_REQUEST, FAILURE_HTML);
46
+ return;
43
47
  }
44
48
  resolveCallback(currentUrl);
45
- return ctx.html(SUCCESS_HTML, HTTP_OK);
49
+ writeHtml(response, HTTP_OK, SUCCESS_HTML);
46
50
  });
47
- app.notFound((ctx) => ctx.html(FAILURE_HTML, HTTP_NOT_FOUND));
48
- const httpServer = serve({
49
- fetch: app.fetch,
50
- hostname: redirectUrl.hostname,
51
- port,
51
+ await new Promise((resolve, reject) => {
52
+ const handleError = (error) => {
53
+ reject(error);
54
+ };
55
+ httpServer.once('error', handleError);
56
+ httpServer.listen(port, redirectUrl.hostname, () => {
57
+ httpServer.off('error', handleError);
58
+ resolve();
59
+ });
52
60
  });
53
61
  const timeout = setTimeout(() => {
54
62
  rejectCallback(new Error('OAuth 登录超时,请重新执行 oauth:login'));
@@ -61,14 +69,17 @@ export async function startOAuthCallbackServer(redirectUri) {
61
69
  hasClosed = true;
62
70
  clearTimeout(timeout);
63
71
  rejectCallback(new Error('OAuth 回调服务已关闭'));
64
- const closableServer = httpServer;
65
72
  await new Promise((resolve) => {
66
73
  httpServer.close(() => {
67
74
  resolve();
68
75
  });
69
- closableServer.closeAllConnections?.();
76
+ httpServer.closeAllConnections();
70
77
  });
71
78
  },
72
79
  };
73
80
  }
81
+ function writeHtml(response, status, html) {
82
+ response.writeHead(status, { 'Content-Type': HTML_CONTENT_TYPE });
83
+ response.end(html);
84
+ }
74
85
  //# sourceMappingURL=callback-server.js.map
package/dist/index.d.ts CHANGED
@@ -2,13 +2,13 @@
2
2
  * @xfe-repo/cli-plugin-oauth - OAuth 登录插件
3
3
  *
4
4
  * 通过 openid-client 对接 OIDC/OAuth 登录,保存本地 token 状态,
5
- * 并向其他插件提供 auth/login/getToken/logout 能力。
5
+ * 并向其他插件提供 login/getToken/getUser/logout 能力。
6
6
  */
7
7
  import type { XfePlugin } from '@xfe-repo/cli-core';
8
- import { type OAuthCapability, type OAuthConfig, type OAuthStoreState, type OAuthTokenState, type OAuthUserState } from './types.js';
9
- export type { OAuthCapability, OAuthConfig, OAuthLoginOptions, OAuthSessionState, OAuthStoreState, OAuthTokenState, OAuthUserState, } from './types.js';
8
+ import { type OAuthCapability, type OAuthStoreState } from './types.js';
9
+ export type { OAuthCapability, OAuthConfig, OAuthLoginOptions, OAuthSessionState, OAuthStoreState, OAuthUserState, } from './types.js';
10
10
  export { OAuthClientAuthMethod } from './types.js';
11
- export declare const oauthStoreInitial: OAuthStoreState;
11
+ declare const oauthStoreInitial: OAuthStoreState;
12
12
  declare module '@xfe-repo/cli-core' {
13
13
  interface XfePluginRegistry {
14
14
  oauth: XfePlugin<typeof oauthStoreInitial, OAuthCapability>;
@@ -16,7 +16,4 @@ declare module '@xfe-repo/cli-core' {
16
16
  }
17
17
  export declare function oauthPlugin(): XfePlugin<typeof oauthStoreInitial, OAuthCapability>;
18
18
  export default oauthPlugin;
19
- export declare function resolveOAuthStateForConfig(config: OAuthConfig, state?: Partial<OAuthStoreState>): OAuthTokenState;
20
- export declare function resolveOAuthConfigKey(config: OAuthConfig): string;
21
- export declare function normalizeOAuthUserInfo(input: unknown): OAuthUserState;
22
19
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * @xfe-repo/cli-plugin-oauth - OAuth 登录插件
3
3
  *
4
4
  * 通过 openid-client 对接 OIDC/OAuth 登录,保存本地 token 状态,
5
- * 并向其他插件提供 auth/login/getToken/logout 能力。
5
+ * 并向其他插件提供 login/getToken/getUser/logout 能力。
6
6
  */
7
7
  import { createHash } from 'node:crypto';
8
8
  import * as oauthClient from 'openid-client';
@@ -25,7 +25,7 @@ const OAUTH_CONFIG_KEY_VERSION = 'v1';
25
25
  const OAUTH_CONFIG_KEY_SEPARATOR = '\n';
26
26
  const OAUTH_CONFIG_HASH_ALGORITHM = 'sha256';
27
27
  const OAUTH_CONFIG_HASH_ENCODING = 'hex';
28
- export const oauthStoreInitial = {};
28
+ const oauthStoreInitial = {};
29
29
  const oauthConfigSchema = z
30
30
  .object({
31
31
  name: z.string().trim().min(1).default(DEFAULT_OAUTH_NAME),
@@ -66,12 +66,6 @@ export function oauthPlugin() {
66
66
  return currentSession?.isAuthenticated ? 'cyan' : 'yellow';
67
67
  },
68
68
  });
69
- hooks.onProjectInit.tap(OAUTH_PLUGIN_NAME, async (ctx) => {
70
- const config = parseOptionalOAuthConfig(ctx.store.getState().xfe);
71
- if (!config)
72
- return;
73
- replaceOAuthStoreState(ctx, createOAuthStoreState(ctx.store.getState().oauth));
74
- });
75
69
  hooks.registerCommand({
76
70
  name: 'oauth:login',
77
71
  simpleDescription: '登录 OAuth 平台',
@@ -89,7 +83,6 @@ export function oauthPlugin() {
89
83
  },
90
84
  });
91
85
  hooks.provide('oauth', {
92
- auth: (ctx, options) => login(ctx, options, runtime),
93
86
  login: (ctx, options) => login(ctx, options, runtime),
94
87
  getToken: (ctx) => getToken(ctx, runtime),
95
88
  getUser: (ctx) => getUser(ctx, runtime),
@@ -108,12 +101,9 @@ export default oauthPlugin;
108
101
  // ─── Capability Handlers ───────────────────────────────────
109
102
  async function login(ctx, options, runtime) {
110
103
  const config = parseOAuthConfig(ctx.store.getState().xfe);
111
- const current = activateOAuthState(ctx, config);
104
+ const current = resolveOAuthStateForConfig(config, ctx.store.getState().oauth);
112
105
  const now = Math.floor(Date.now() / MILLISECONDS_PER_SECOND);
113
- if (!options?.force &&
114
- current.isAuthenticated &&
115
- current.accessToken &&
116
- (!current.expiresAt || current.expiresAt - TOKEN_EXPIRY_SKEW_SECONDS > now)) {
106
+ if (!options?.force && isOAuthAccessTokenValid(current, now)) {
117
107
  return current;
118
108
  }
119
109
  const spinner = ctx.spinner.create(`正在登录 ${config.name}...`);
@@ -151,19 +141,7 @@ async function login(ctx, options, runtime) {
151
141
  }
152
142
  if (!tokenResponse.access_token)
153
143
  throw new Error('OAuth token 响应缺少 access_token');
154
- const expiresIn = tokenResponse.expiresIn();
155
- const tokenState = {
156
- name: config.name,
157
- accessToken: tokenResponse.access_token,
158
- refreshToken: tokenResponse.refresh_token || '',
159
- idToken: tokenResponse.id_token || '',
160
- tokenType: tokenResponse.token_type || DEFAULT_TOKEN_TYPE,
161
- scope: tokenResponse.scope || '',
162
- expiresAt: expiresIn ? now + expiresIn : 0,
163
- isAuthenticated: true,
164
- userId: 0,
165
- userName: '',
166
- };
144
+ const tokenState = createOAuthSessionFromToken(config, tokenResponse, now);
167
145
  saveOAuthSession(ctx, config, tokenState);
168
146
  spinner.succeed(`${config.name} 登录成功`);
169
147
  return resolveOAuthStateForConfig(config, ctx.store.getState().oauth);
@@ -176,9 +154,9 @@ async function login(ctx, options, runtime) {
176
154
  }
177
155
  async function getToken(ctx, runtime) {
178
156
  const config = parseOAuthConfig(ctx.store.getState().xfe);
179
- const current = activateOAuthState(ctx, config);
157
+ const current = resolveOAuthStateForConfig(config, ctx.store.getState().oauth);
180
158
  const now = Math.floor(Date.now() / MILLISECONDS_PER_SECOND);
181
- if (current.isAuthenticated && current.accessToken && (!current.expiresAt || current.expiresAt - TOKEN_EXPIRY_SKEW_SECONDS > now)) {
159
+ if (isOAuthAccessTokenValid(current, now)) {
182
160
  return current.accessToken;
183
161
  }
184
162
  if (current.refreshToken) {
@@ -187,19 +165,7 @@ async function getToken(ctx, runtime) {
187
165
  const tokenResponse = await oauthClient.refreshTokenGrant(clientConfig, current.refreshToken);
188
166
  if (!tokenResponse.access_token)
189
167
  throw new Error('OAuth token 响应缺少 access_token');
190
- const expiresIn = tokenResponse.expiresIn();
191
- const tokenState = {
192
- name: config.name,
193
- accessToken: tokenResponse.access_token,
194
- refreshToken: tokenResponse.refresh_token || current.refreshToken,
195
- idToken: tokenResponse.id_token || '',
196
- tokenType: tokenResponse.token_type || DEFAULT_TOKEN_TYPE,
197
- scope: tokenResponse.scope || '',
198
- expiresAt: expiresIn ? now + expiresIn : 0,
199
- isAuthenticated: true,
200
- userId: 0,
201
- userName: '',
202
- };
168
+ const tokenState = createOAuthSessionFromToken(config, tokenResponse, now, current.refreshToken);
203
169
  saveOAuthSession(ctx, config, tokenState);
204
170
  return tokenState.accessToken;
205
171
  }
@@ -213,7 +179,7 @@ async function getToken(ctx, runtime) {
213
179
  }
214
180
  async function getUser(ctx, runtime) {
215
181
  const config = parseOAuthConfig(ctx.store.getState().xfe);
216
- const current = activateOAuthState(ctx, config);
182
+ const current = resolveOAuthStateForConfig(config, ctx.store.getState().oauth);
217
183
  if (current.userId > 0 && current.userName)
218
184
  return { userId: current.userId, userName: current.userName };
219
185
  if (!config.userInfoUrl)
@@ -238,14 +204,14 @@ async function logout(ctx) {
238
204
  const config = parseOptionalOAuthConfig(ctx.store.getState().xfe);
239
205
  const current = config ? resolveOAuthStateForConfig(config, ctx.store.getState().oauth) : undefined;
240
206
  const name = config?.name || current?.name || DEFAULT_OAUTH_NAME;
241
- const storeState = createOAuthStoreState(ctx.store.getState().oauth);
207
+ const storeState = cloneOAuthState(ctx.store.getState().oauth);
242
208
  if (config)
243
209
  delete storeState[resolveOAuthConfigKey(config)];
244
- replaceOAuthStoreState(ctx, storeState);
210
+ ctx.store.setState({ oauth: storeState });
245
211
  ctx.logger.success(`${name} 已退出登录`);
246
212
  }
247
213
  // ─── Config And State ──────────────────────────────────────
248
- export function resolveOAuthStateForConfig(config, state) {
214
+ function resolveOAuthStateForConfig(config, state) {
249
215
  const configKey = resolveOAuthConfigKey(config);
250
216
  const session = state?.[configKey];
251
217
  return session ? toOAuthSessionState(session, config.name) : createEmptyOAuthSession(config.name);
@@ -267,19 +233,11 @@ function parseOptionalOAuthConfig(xfeConfig) {
267
233
  }
268
234
  return result.data;
269
235
  }
270
- function activateOAuthState(ctx, config) {
271
- const storeState = createOAuthStoreState(ctx.store.getState().oauth);
272
- replaceOAuthStoreState(ctx, storeState);
273
- return resolveOAuthStateForConfig(config, storeState);
274
- }
275
236
  function saveOAuthSession(ctx, config, session) {
276
237
  const configKey = resolveOAuthConfigKey(config);
277
- const storeState = createOAuthStoreState(ctx.store.getState().oauth);
238
+ const storeState = cloneOAuthState(ctx.store.getState().oauth);
278
239
  storeState[configKey] = toOAuthSessionState(session, config.name);
279
- replaceOAuthStoreState(ctx, storeState);
280
- }
281
- function replaceOAuthStoreState(ctx, state) {
282
- ctx.store.setState({ oauth: state });
240
+ ctx.store.setState({ oauth: storeState });
283
241
  }
284
242
  function resolveOptionalOAuthState(xfeConfig, state) {
285
243
  const config = parseOptionalOAuthConfig(xfeConfig);
@@ -287,10 +245,7 @@ function resolveOptionalOAuthState(xfeConfig, state) {
287
245
  return undefined;
288
246
  return resolveOAuthStateForConfig(config, state);
289
247
  }
290
- function createOAuthStoreState(state) {
291
- return cloneOAuthState(state);
292
- }
293
- export function resolveOAuthConfigKey(config) {
248
+ function resolveOAuthConfigKey(config) {
294
249
  const payload = [
295
250
  OAUTH_CONFIG_KEY_VERSION,
296
251
  config.discoveryUrl ?? '',
@@ -335,7 +290,25 @@ function toOAuthSessionState(state, name) {
335
290
  userName: state?.userName || '',
336
291
  };
337
292
  }
338
- export function normalizeOAuthUserInfo(input) {
293
+ function isOAuthAccessTokenValid(state, now) {
294
+ return state.isAuthenticated && !!state.accessToken && (!state.expiresAt || state.expiresAt - TOKEN_EXPIRY_SKEW_SECONDS > now);
295
+ }
296
+ function createOAuthSessionFromToken(config, tokenResponse, now, fallbackRefreshToken = '') {
297
+ const expiresIn = tokenResponse.expiresIn();
298
+ return {
299
+ name: config.name,
300
+ accessToken: tokenResponse.access_token ?? '',
301
+ refreshToken: tokenResponse.refresh_token || fallbackRefreshToken,
302
+ idToken: tokenResponse.id_token || '',
303
+ tokenType: tokenResponse.token_type || DEFAULT_TOKEN_TYPE,
304
+ scope: tokenResponse.scope || '',
305
+ expiresAt: expiresIn ? now + expiresIn : 0,
306
+ isAuthenticated: true,
307
+ userId: 0,
308
+ userName: '',
309
+ };
310
+ }
311
+ function normalizeOAuthUserInfo(input) {
339
312
  if (typeof input !== 'object' || input === null || Array.isArray(input)) {
340
313
  throw new Error('OAuth 用户信息响应格式无效');
341
314
  }
package/dist/types.d.ts CHANGED
@@ -33,7 +33,6 @@ export interface OAuthSessionState {
33
33
  userName: string;
34
34
  }
35
35
  export type OAuthStoreState = Record<string, OAuthSessionState>;
36
- export type OAuthTokenState = OAuthSessionState;
37
36
  export interface OAuthLoginOptions {
38
37
  force?: boolean;
39
38
  }
@@ -42,8 +41,7 @@ export interface OAuthUserState {
42
41
  userName: string;
43
42
  }
44
43
  export interface OAuthCapability {
45
- auth(ctx: PluginContext, options?: OAuthLoginOptions): Promise<OAuthTokenState>;
46
- login(ctx: PluginContext, options?: OAuthLoginOptions): Promise<OAuthTokenState>;
44
+ login(ctx: PluginContext, options?: OAuthLoginOptions): Promise<OAuthSessionState>;
47
45
  getToken(ctx: PluginContext): Promise<string>;
48
46
  getUser(ctx: PluginContext): Promise<OAuthUserState>;
49
47
  logout(ctx: PluginContext): Promise<void>;
package/package.json CHANGED
@@ -1,13 +1,11 @@
1
1
  {
2
2
  "name": "@xfe-repo/cli-plugin-oauth",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
4
4
  "description": "XFE OAuth plugin - login through OIDC/OAuth and expose token state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "dependencies": {
9
- "@hono/node-server": "^1.14.0",
10
- "hono": "^4.7.0",
11
9
  "openid-client": "^6.8.1",
12
10
  "open": "^10.2.0",
13
11
  "zod": "^4.3.6",