@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 +6 -0
- package/dist/callback-server.d.ts +1 -1
- package/dist/callback-server.js +26 -15
- package/dist/index.d.ts +4 -7
- package/dist/index.js +34 -61
- package/dist/types.d.ts +1 -3
- package/package.json +1 -3
package/CHANGELOG.md
CHANGED
package/dist/callback-server.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @xfe-repo/cli-plugin-oauth - OAuth 回调服务
|
|
3
3
|
*
|
|
4
|
-
* 基于
|
|
4
|
+
* 基于 Node HTTP 启动临时本地服务,等待 OAuth 授权服务器回跳 redirectUri。
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
45
|
+
writeHtml(response, HTTP_BAD_REQUEST, FAILURE_HTML);
|
|
46
|
+
return;
|
|
43
47
|
}
|
|
44
48
|
resolveCallback(currentUrl);
|
|
45
|
-
|
|
49
|
+
writeHtml(response, HTTP_OK, SUCCESS_HTML);
|
|
46
50
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
* 并向其他插件提供
|
|
5
|
+
* 并向其他插件提供 login/getToken/getUser/logout 能力。
|
|
6
6
|
*/
|
|
7
7
|
import type { XfePlugin } from '@xfe-repo/cli-core';
|
|
8
|
-
import { type OAuthCapability, type
|
|
9
|
-
export type { OAuthCapability, OAuthConfig, OAuthLoginOptions, OAuthSessionState, OAuthStoreState,
|
|
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
|
-
|
|
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
|
-
* 并向其他插件提供
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
157
|
+
const current = resolveOAuthStateForConfig(config, ctx.store.getState().oauth);
|
|
180
158
|
const now = Math.floor(Date.now() / MILLISECONDS_PER_SECOND);
|
|
181
|
-
if (
|
|
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
|
|
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 =
|
|
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 =
|
|
207
|
+
const storeState = cloneOAuthState(ctx.store.getState().oauth);
|
|
242
208
|
if (config)
|
|
243
209
|
delete storeState[resolveOAuthConfigKey(config)];
|
|
244
|
-
|
|
210
|
+
ctx.store.setState({ oauth: storeState });
|
|
245
211
|
ctx.logger.success(`${name} 已退出登录`);
|
|
246
212
|
}
|
|
247
213
|
// ─── Config And State ──────────────────────────────────────
|
|
248
|
-
|
|
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 =
|
|
238
|
+
const storeState = cloneOAuthState(ctx.store.getState().oauth);
|
|
278
239
|
storeState[configKey] = toOAuthSessionState(session, config.name);
|
|
279
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|