@morehao/dsh-github-trending 0.8.2 → 0.8.4
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 +17 -0
- package/README.md +2 -2
- package/client.js +27 -10
- package/lib/{chunk-5D2VRWPJ.js → chunk-QOYTABJH.js} +107 -15
- package/lib/index.d.ts +1 -0
- package/lib/index.js +5 -2
- package/lib/internal.d.ts +44 -3
- package/lib/internal.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v0.8.4] - 2026-11-02
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **auth**: open device-code page with prefilled code
|
|
8
|
+
- **auth**: 美化 GitHub OAuth 回调落地页 (#78)
|
|
9
|
+
|
|
10
|
+
**Full Changelog**: https://github.com/morehao/dsh-github-trending/compare/v0.8.3...v0.8.4
|
|
11
|
+
|
|
12
|
+
## [v0.8.3] - 2026-11-02
|
|
13
|
+
|
|
14
|
+
### Fixes
|
|
15
|
+
|
|
16
|
+
- **auth**: fall back to device flow when client_secret is missing (#75)
|
|
17
|
+
|
|
18
|
+
**Full Changelog**: https://github.com/morehao/dsh-github-trending/compare/v0.8.2...v0.8.3
|
|
19
|
+
|
|
3
20
|
## [v0.8.2] - 2026-11-02
|
|
4
21
|
|
|
5
22
|
### Fixes
|
package/README.md
CHANGED
|
@@ -59,13 +59,13 @@ GitHub 趋势榜的 **DeepSeek Harness 插件**:按日 / 周 / 月周期定时
|
|
|
59
59
|
|
|
60
60
|
认证分区支持三种方式(可随时切换):
|
|
61
61
|
|
|
62
|
-
- **浏览器登录(默认推荐)**:点「用 GitHub 登录」在浏览器完成授权,无需手工粘贴 token
|
|
62
|
+
- **浏览器登录(默认推荐)**:点「用 GitHub 登录」在浏览器完成授权,无需手工粘贴 token,token 由 DSH 凭据记录安全保存、不落 `settings.json`。插件内置了公开的 OAuth App client_id,开箱即用;GitHub 的 web flow 换 token 仍然**必须带 client_secret**(PKCE 只是叠加保护),而客户端不能内置机密,所以默认走**设备码**:点一下后在打开的 GitHub 页面输入验证码即可。若你自带 OAuth App,可在插件配置里同时给 `githubClientId` + `githubClientSecret`,就会改用本机回环回调(不用手输验证码)。
|
|
63
63
|
- **本机 gh**:复用你终端里 `gh auth login` 的登录态。
|
|
64
64
|
- **PAT**:手工填 Personal Access Token(存到凭据引用 / 环境变量)。
|
|
65
65
|
|
|
66
66
|
> 浏览器登录需要核心提供 `authorization` 服务,并且 GitHub flow 注册成功才算可用;两者任一不满足时,设置页直接提示不可用并引导改用 gh / PAT,而不是给一个点了没反应的按钮。宿主启动时服务可能晚于插件就绪,插件会等服务就绪再注册,并在读状态时再兜底一次。
|
|
67
67
|
>
|
|
68
|
-
>
|
|
68
|
+
> 设备码 / 授权页默认尝试自动打开;若被浏览器的弹窗拦截,状态行里始终有可点的「打开授权页」链接、验证码与「复制链接」。
|
|
69
69
|
|
|
70
70
|
### 订阅
|
|
71
71
|
|
package/client.js
CHANGED
|
@@ -125,10 +125,10 @@ var DICT = {
|
|
|
125
125
|
"auth.gh": "本机 gh",
|
|
126
126
|
"auth.browser": "浏览器登录",
|
|
127
127
|
"auth.gh.hint": "使用本机 gh 登录态:终端执行 `gh auth login` 完成登录,再点「测试认证」。",
|
|
128
|
-
"auth.browser.hint": "在浏览器里完成 GitHub 授权,token 由 DSH
|
|
128
|
+
"auth.browser.hint": "在浏览器里完成 GitHub 授权,token 由 DSH 安全保存,无需手工粘贴。点一下会在浏览器打开授权页(验证码通常已自动填入),确认授权即可。",
|
|
129
129
|
"auth.browser.signin": "用 GitHub 登录",
|
|
130
130
|
"auth.browser.waiting": "等待浏览器授权…",
|
|
131
|
-
"auth.browser.code": "
|
|
131
|
+
"auth.browser.code": "如未自动填入,请手动输入验证码:",
|
|
132
132
|
"auth.browser.cancel": "取消",
|
|
133
133
|
"auth.browser.done": "已登录",
|
|
134
134
|
"auth.browser.device": "用设备码登录",
|
|
@@ -139,7 +139,7 @@ var DICT = {
|
|
|
139
139
|
"auth.browser.copied": "授权链接已复制",
|
|
140
140
|
"auth.browser.failed": "登录失败",
|
|
141
141
|
"auth.browser.failed.detail": "登录失败:{message}",
|
|
142
|
-
"auth.browser.failed.hint": "
|
|
142
|
+
"auth.browser.failed.hint": "请重试一次;仍失败可改用「本机 gh」或 PAT。",
|
|
143
143
|
"auth.pat.hint": "在 GitHub → Settings → Developer settings 生成 Personal Access Token(勾选 public_repo),填到下方引用或环境变量。",
|
|
144
144
|
"auth.tokenRef": "凭据引用",
|
|
145
145
|
"auth.tokenRef.hint": "凭据引用名 / 环境变量名",
|
|
@@ -670,10 +670,10 @@ var DICT = {
|
|
|
670
670
|
"auth.gh": "Local gh",
|
|
671
671
|
"auth.browser": "Browser login",
|
|
672
672
|
"auth.gh.hint": "Uses your local gh login: run `gh auth login` in a terminal, then click Test authentication.",
|
|
673
|
-
"auth.browser.hint": "Authorize GitHub in your browser; DSH stores the token securely, no manual paste.",
|
|
673
|
+
"auth.browser.hint": "Authorize GitHub in your browser; DSH stores the token securely, no manual paste. Clicking opens the authorization page with the code usually pre-filled — just confirm.",
|
|
674
674
|
"auth.browser.signin": "Sign in with GitHub",
|
|
675
675
|
"auth.browser.waiting": "Waiting for browser authorization…",
|
|
676
|
-
"auth.browser.code": "
|
|
676
|
+
"auth.browser.code": "If it is not filled in automatically, enter this code:",
|
|
677
677
|
"auth.browser.cancel": "Cancel",
|
|
678
678
|
"auth.browser.done": "Signed in",
|
|
679
679
|
"auth.browser.device": "Use a device code",
|
|
@@ -684,7 +684,7 @@ var DICT = {
|
|
|
684
684
|
"auth.browser.copied": "Authorization link copied",
|
|
685
685
|
"auth.browser.failed": "Sign-in failed",
|
|
686
686
|
"auth.browser.failed.detail": "Sign-in failed: {message}",
|
|
687
|
-
"auth.browser.failed.hint": "
|
|
687
|
+
"auth.browser.failed.hint": "Retry once; if it keeps failing, use Local gh or a PAT instead.",
|
|
688
688
|
"auth.pat.hint": "Create a Personal Access Token under GitHub → Settings → Developer settings (scope public_repo) and put it in the ref below or an env var.",
|
|
689
689
|
"auth.tokenRef": "Credential ref",
|
|
690
690
|
"auth.tokenRef.hint": "credential ref / env var name",
|
|
@@ -2272,6 +2272,13 @@ function SettingsView(props) {
|
|
|
2272
2272
|
} catch {
|
|
2273
2273
|
}
|
|
2274
2274
|
}
|
|
2275
|
+
const deviceUrl = view.verificationUriComplete ?? view.verificationUri;
|
|
2276
|
+
if (view.phase === "waiting-device" && deviceUrl !== void 0) {
|
|
2277
|
+
try {
|
|
2278
|
+
window.open(deviceUrl, "_blank", "noopener,noreferrer");
|
|
2279
|
+
} catch {
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2275
2282
|
} catch (error) {
|
|
2276
2283
|
notify("err", error instanceof Error ? error.message : String(error));
|
|
2277
2284
|
}
|
|
@@ -2348,6 +2355,7 @@ function SettingsView(props) {
|
|
|
2348
2355
|
const isGh = draft.github.authMode === "gh";
|
|
2349
2356
|
const isBrowser = draft.github.authMode === "browser";
|
|
2350
2357
|
const browserAvailable = state.github.browserAvailable === true;
|
|
2358
|
+
const loopbackAvailable = state.github.loopbackAvailable === true;
|
|
2351
2359
|
const sections = [
|
|
2352
2360
|
{ id: "source", label: t("st.source") },
|
|
2353
2361
|
{ id: "model", label: t("st.model") },
|
|
@@ -2542,12 +2550,12 @@ function SettingsView(props) {
|
|
|
2542
2550
|
{
|
|
2543
2551
|
small: true,
|
|
2544
2552
|
variant: "primary",
|
|
2545
|
-
onClick: () => void startSignIn("browser"),
|
|
2553
|
+
onClick: () => void startSignIn(loopbackAvailable ? "browser" : "device"),
|
|
2546
2554
|
disabled: signInActive
|
|
2547
2555
|
},
|
|
2548
2556
|
signIn.phase === "done" && signIn.login !== void 0 ? `${t("auth.browser.done")} · @${signIn.login}` : signInActive ? t("auth.browser.waiting") : t("auth.browser.signin")
|
|
2549
2557
|
) : h("span", { className: "dshgt-hint" }, t("auth.browser.unavailable")),
|
|
2550
|
-
browserAvailable ? h(Btn, { small: true, variant: "ghost", onClick: () => void startSignIn("device"), disabled: signInActive }, t("auth.browser.device")) : null,
|
|
2558
|
+
browserAvailable && loopbackAvailable ? h(Btn, { small: true, variant: "ghost", onClick: () => void startSignIn("device"), disabled: signInActive }, t("auth.browser.device")) : null,
|
|
2551
2559
|
signInActive ? h(Btn, { small: true, variant: "ghost", onClick: () => void cancelSignIn() }, t("auth.browser.cancel")) : null
|
|
2552
2560
|
) : h(
|
|
2553
2561
|
Row,
|
|
@@ -2576,10 +2584,19 @@ function SettingsView(props) {
|
|
|
2576
2584
|
" ",
|
|
2577
2585
|
h(Btn, { small: true, variant: "ghost", onClick: () => copyAuthorizeLink() }, t("auth.browser.copy"))
|
|
2578
2586
|
) : null,
|
|
2579
|
-
signIn.verificationUri !== void 0 ? h(
|
|
2587
|
+
signIn.verificationUriComplete !== void 0 || signIn.verificationUri !== void 0 ? h(
|
|
2580
2588
|
"div",
|
|
2581
2589
|
{ className: "dshgt-desc" },
|
|
2582
|
-
h(
|
|
2590
|
+
h(
|
|
2591
|
+
"a",
|
|
2592
|
+
{
|
|
2593
|
+
className: "dshgt-link",
|
|
2594
|
+
href: signIn.verificationUriComplete ?? signIn.verificationUri,
|
|
2595
|
+
target: "_blank",
|
|
2596
|
+
rel: "noreferrer"
|
|
2597
|
+
},
|
|
2598
|
+
t("auth.browser.open")
|
|
2599
|
+
)
|
|
2583
2600
|
) : null,
|
|
2584
2601
|
signIn.userCode !== void 0 ? h("div", { className: "dshgt-mono" }, `${t("auth.browser.code")} ${signIn.userCode}`) : null
|
|
2585
2602
|
) : null,
|
|
@@ -3540,9 +3540,14 @@ __export(github_auth_exports, {
|
|
|
3540
3540
|
createPkce: () => createPkce,
|
|
3541
3541
|
ensureGithubFlow: () => ensureGithubFlow,
|
|
3542
3542
|
isGithubFlowRegistered: () => isGithubFlowRegistered,
|
|
3543
|
+
isHttpUrl: () => isHttpUrl,
|
|
3544
|
+
isLoopbackSignInAvailable: () => isLoopbackSignInAvailable,
|
|
3543
3545
|
registerGithubFlow: () => registerGithubFlow,
|
|
3546
|
+
renderCallbackPage: () => renderCallbackPage,
|
|
3544
3547
|
resetAttemptForTest: () => resetAttemptForTest,
|
|
3545
3548
|
resolveClientId: () => resolveClientId,
|
|
3549
|
+
resolveClientSecret: () => resolveClientSecret,
|
|
3550
|
+
resolveMethod: () => resolveMethod,
|
|
3546
3551
|
verifyState: () => verifyState
|
|
3547
3552
|
});
|
|
3548
3553
|
import { createHash as createHash3, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
@@ -3553,6 +3558,16 @@ function resolveClientId(deps) {
|
|
|
3553
3558
|
const fromConfig = deps.clientId;
|
|
3554
3559
|
return fromConfig !== void 0 && fromConfig !== "" ? fromConfig : DEFAULT_GITHUB_CLIENT_ID;
|
|
3555
3560
|
}
|
|
3561
|
+
function resolveClientSecret(deps) {
|
|
3562
|
+
const secret = deps.clientSecret;
|
|
3563
|
+
return secret !== void 0 && secret !== "" ? secret : void 0;
|
|
3564
|
+
}
|
|
3565
|
+
function isLoopbackSignInAvailable(deps) {
|
|
3566
|
+
return resolveClientSecret(deps) !== void 0;
|
|
3567
|
+
}
|
|
3568
|
+
function resolveMethod(deps, requested) {
|
|
3569
|
+
return requested === "browser" && !isLoopbackSignInAvailable(deps) ? "device" : requested;
|
|
3570
|
+
}
|
|
3556
3571
|
var AUTHORIZE_URL = "https://github.com/login/oauth/authorize";
|
|
3557
3572
|
var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
3558
3573
|
var DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
@@ -3568,6 +3583,16 @@ function attemptView() {
|
|
|
3568
3583
|
function base64url(buf) {
|
|
3569
3584
|
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
3570
3585
|
}
|
|
3586
|
+
function isHttpUrl(value) {
|
|
3587
|
+
if (typeof value !== "string" || value === "") return false;
|
|
3588
|
+
let url;
|
|
3589
|
+
try {
|
|
3590
|
+
url = new URL(value);
|
|
3591
|
+
} catch {
|
|
3592
|
+
return false;
|
|
3593
|
+
}
|
|
3594
|
+
return url.protocol === "https:" || url.protocol === "http:";
|
|
3595
|
+
}
|
|
3571
3596
|
function createPkce() {
|
|
3572
3597
|
const verifier = base64url(randomBytes2(32));
|
|
3573
3598
|
const challenge = base64url(createHash3("sha256").update(verifier).digest());
|
|
@@ -3578,6 +3603,41 @@ function verifyState(received, expected) {
|
|
|
3578
3603
|
const b = Buffer.from(expected);
|
|
3579
3604
|
return a.length === b.length && timingSafeEqual2(a, b);
|
|
3580
3605
|
}
|
|
3606
|
+
function renderCallbackPage(kind) {
|
|
3607
|
+
const ok = kind === "success";
|
|
3608
|
+
const title = ok ? "\u767B\u5F55\u5B8C\u6210" : "\u767B\u5F55\u672A\u5B8C\u6210";
|
|
3609
|
+
const heading = ok ? "GitHub \u6388\u6743\u6210\u529F" : "GitHub \u6388\u6743\u5931\u8D25";
|
|
3610
|
+
const body = ok ? "\u5DF2\u5B8C\u6210\u767B\u5F55\uFF0C\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\u3002" : "\u6388\u6743\u6821\u9A8C\u672A\u901A\u8FC7\uFF0C\u8BF7\u56DE\u5230 DSH \u91CD\u65B0\u53D1\u8D77\u767B\u5F55\u3002";
|
|
3611
|
+
const accent = ok ? "#1f883d" : "#cf222e";
|
|
3612
|
+
const icon = ok ? '<path d="M20 6 9 17l-5-5"/>' : '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>';
|
|
3613
|
+
const autoClose = ok ? "<script>(function(){try{setTimeout(function(){window.close()},1200)}catch(e){}})()</script>" : "";
|
|
3614
|
+
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
|
3615
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
3616
|
+
<meta name="color-scheme" content="light dark">
|
|
3617
|
+
<title>${title}</title>
|
|
3618
|
+
<style>
|
|
3619
|
+
:root{--bg:#f6f8fa;--card:#ffffff;--fg:#1f2328;--muted:#59636e;--border:#d1d9e0;--accent:${accent}}
|
|
3620
|
+
@media (prefers-color-scheme:dark){:root{--bg:#0d1117;--card:#151b23;--fg:#e6edf3;--muted:#9198a1;--border:#3d444d}}
|
|
3621
|
+
*{box-sizing:border-box}
|
|
3622
|
+
html,body{height:100%}
|
|
3623
|
+
body{margin:0;display:flex;align-items:center;justify-content:center;padding:24px;
|
|
3624
|
+
background:var(--bg);color:var(--fg);
|
|
3625
|
+
font:400 15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif}
|
|
3626
|
+
main{width:100%;max-width:400px;background:var(--card);border:1px solid var(--border);
|
|
3627
|
+
border-radius:14px;padding:40px 32px;text-align:center;
|
|
3628
|
+
box-shadow:0 8px 24px rgba(0,0,0,.06)}
|
|
3629
|
+
.mark{width:64px;height:64px;margin:0 auto 20px;border-radius:50%;display:flex;
|
|
3630
|
+
align-items:center;justify-content:center;background:color-mix(in srgb,var(--accent) 14%,transparent);color:var(--accent)}
|
|
3631
|
+
.mark svg{width:30px;height:30px;fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
|
|
3632
|
+
h1{margin:0 0 8px;font-size:19px;font-weight:600;letter-spacing:.2px}
|
|
3633
|
+
p{margin:0;color:var(--muted);font-size:14px}
|
|
3634
|
+
</style></head>
|
|
3635
|
+
<body><main role="status">
|
|
3636
|
+
<div class="mark"><svg viewBox="0 0 24 24" aria-hidden="true">${icon}</svg></div>
|
|
3637
|
+
<h1>${heading}</h1>
|
|
3638
|
+
<p>${body}</p>
|
|
3639
|
+
</main>${autoClose}</body></html>`;
|
|
3640
|
+
}
|
|
3581
3641
|
function buildAuthorizeUrl(params) {
|
|
3582
3642
|
const url = new URL(params.authorizeEndpoint);
|
|
3583
3643
|
url.searchParams.set("client_id", params.clientId);
|
|
@@ -3589,21 +3649,29 @@ function buildAuthorizeUrl(params) {
|
|
|
3589
3649
|
return url.toString();
|
|
3590
3650
|
}
|
|
3591
3651
|
async function exchangeCode(fetchImpl, endpoint, params, signal) {
|
|
3652
|
+
const form = new URLSearchParams({
|
|
3653
|
+
client_id: params.clientId,
|
|
3654
|
+
code: params.code,
|
|
3655
|
+
code_verifier: params.verifier,
|
|
3656
|
+
redirect_uri: params.redirectUri
|
|
3657
|
+
});
|
|
3658
|
+
if (params.clientSecret !== void 0) form.set("client_secret", params.clientSecret);
|
|
3592
3659
|
const response = await fetchImpl(endpoint, {
|
|
3593
3660
|
method: "POST",
|
|
3594
3661
|
headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
|
|
3595
|
-
body:
|
|
3596
|
-
client_id: params.clientId,
|
|
3597
|
-
code: params.code,
|
|
3598
|
-
code_verifier: params.verifier,
|
|
3599
|
-
redirect_uri: params.redirectUri
|
|
3600
|
-
}).toString(),
|
|
3662
|
+
body: form.toString(),
|
|
3601
3663
|
signal
|
|
3602
3664
|
});
|
|
3603
3665
|
if (!response.ok) throw new Error(`GitHub access_token HTTP ${response.status}`);
|
|
3604
3666
|
const body = await response.json();
|
|
3605
3667
|
if (typeof body.access_token !== "string" || body.access_token === "") {
|
|
3606
|
-
|
|
3668
|
+
const error = typeof body.error === "string" ? body.error : void 0;
|
|
3669
|
+
if (error === "incorrect_client_credentials") {
|
|
3670
|
+
throw new Error(
|
|
3671
|
+
params.clientSecret === void 0 ? "GitHub \u7F51\u9875\u6388\u6743\u9700\u8981 client_secret\uFF08incorrect_client_credentials\uFF09\uFF0C\u5185\u7F6E\u7684\u516C\u5F00 client_id \u65E0\u6CD5\u5B8C\u6210\u8FD9\u4E00\u6B65\uFF1A\u8BF7\u6539\u7528\u8BBE\u5907\u7801\u767B\u5F55\uFF0C\u6216\u914D\u7F6E\u81EA\u5DF1\u7684 OAuth App client_id + client_secret\u3002" : "GitHub \u8BA4\u4E3A\u8FD9\u4E2A OAuth App \u7684 client_id / client_secret \u4E0D\u6B63\u786E\uFF08incorrect_client_credentials\uFF09\uFF1A\u8BF7\u68C0\u67E5\u63D2\u4EF6\u914D\u7F6E\uFF0C\u6216\u6539\u7528\u8BBE\u5907\u7801\u767B\u5F55\u3002"
|
|
3672
|
+
);
|
|
3673
|
+
}
|
|
3674
|
+
throw new Error(`GitHub access_token \u672A\u8FD4\u56DE token${error === void 0 ? "" : `\uFF08${error}\uFF09`}`);
|
|
3607
3675
|
}
|
|
3608
3676
|
return {
|
|
3609
3677
|
token: body.access_token,
|
|
@@ -3631,6 +3699,10 @@ async function runBrowser(deps, session) {
|
|
|
3631
3699
|
}
|
|
3632
3700
|
const clientId = resolveClientId(deps);
|
|
3633
3701
|
if (clientId === void 0 || clientId === "") throw new Error("\u7F3A\u5C11 GitHub OAuth client_id");
|
|
3702
|
+
const clientSecret = resolveClientSecret(deps);
|
|
3703
|
+
if (clientSecret === void 0) {
|
|
3704
|
+
throw new Error("\u56DE\u73AF\u767B\u5F55\u4E0D\u53EF\u7528\uFF1A\u6CA1\u6709\u914D\u7F6E GitHub OAuth App \u7684 client_secret\uFF0C\u8BF7\u6539\u7528\u8BBE\u5907\u7801\u767B\u5F55");
|
|
3705
|
+
}
|
|
3634
3706
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
3635
3707
|
const endpoints = normalizeEndpoints(deps.endpoints);
|
|
3636
3708
|
const scope = deps.scope ?? DEFAULT_SCOPE;
|
|
@@ -3659,10 +3731,10 @@ async function runBrowser(deps, session) {
|
|
|
3659
3731
|
const single = url.searchParams.getAll("state").length === 1 && url.searchParams.getAll("code").length === 1;
|
|
3660
3732
|
const bad = req.method !== "GET" || url.pathname !== CALLBACK_PATH || !single || code === "" || !verifyState(gotState, state);
|
|
3661
3733
|
if (bad) {
|
|
3662
|
-
res.writeHead(400, { "cache-control": "no-store" }).end();
|
|
3734
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(renderCallbackPage("error"));
|
|
3663
3735
|
return;
|
|
3664
3736
|
}
|
|
3665
|
-
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(
|
|
3737
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(renderCallbackPage("success"));
|
|
3666
3738
|
resolveCode(code);
|
|
3667
3739
|
}
|
|
3668
3740
|
});
|
|
@@ -3681,7 +3753,12 @@ async function runBrowser(deps, session) {
|
|
|
3681
3753
|
session.notify?.({ message: "Continue in your browser to sign in to GitHub.", url: authorizeUrl });
|
|
3682
3754
|
const code = await withAbort(codePromise, session.signal, () => rejectCode(new Error("authorization cancelled")));
|
|
3683
3755
|
updateCurrent({ phase: "exchanging" });
|
|
3684
|
-
const exchanged = await exchangeCode(
|
|
3756
|
+
const exchanged = await exchangeCode(
|
|
3757
|
+
fetchImpl,
|
|
3758
|
+
endpoints.accessToken,
|
|
3759
|
+
{ clientId, clientSecret, code, verifier, redirectUri },
|
|
3760
|
+
session.signal
|
|
3761
|
+
);
|
|
3685
3762
|
const login = await fetchLogin(fetchImpl, endpoints.user, exchanged.token, session.signal);
|
|
3686
3763
|
const payload = {
|
|
3687
3764
|
version: 1,
|
|
@@ -3711,21 +3788,26 @@ async function runDevice(deps, session) {
|
|
|
3711
3788
|
if (!startResponse.ok) throw new Error(`GitHub device/code HTTP ${startResponse.status}`);
|
|
3712
3789
|
const start = await startResponse.json();
|
|
3713
3790
|
if (typeof start.device_code !== "string" || typeof start.user_code !== "string" || typeof start.verification_uri !== "string") {
|
|
3714
|
-
|
|
3791
|
+
const error = typeof start.error === "string" ? start.error : void 0;
|
|
3792
|
+
throw new Error(
|
|
3793
|
+
error === "device_flow_disabled" ? "GitHub \u6CA1\u6709\u4E3A\u8FD9\u4E2A OAuth App \u542F\u7528\u8BBE\u5907\u7801\u767B\u5F55\uFF1A\u8BF7\u5230 OAuth App \u8BBE\u7F6E\u52FE\u9009 Device flow\uFF0C\u6216\u6539\u7528\u300C\u672C\u673A gh\u300D/ PAT\u3002" : `GitHub device/code \u8FD4\u56DE\u4E0D\u5B8C\u6574${error === void 0 ? "" : `\uFF08${error}\uFF09`}`
|
|
3794
|
+
);
|
|
3715
3795
|
}
|
|
3716
3796
|
const deviceCode = start.device_code;
|
|
3717
3797
|
const intervalSec = typeof start.interval === "number" && start.interval > 0 ? start.interval : 5;
|
|
3718
3798
|
const expiresInSec = typeof start.expires_in === "number" && start.expires_in > 0 ? start.expires_in : 900;
|
|
3719
3799
|
const deadline = Date.now() + expiresInSec * 1e3;
|
|
3800
|
+
const verificationUriComplete = isHttpUrl(start.verification_uri_complete) ? start.verification_uri_complete : void 0;
|
|
3720
3801
|
updateCurrent({
|
|
3721
3802
|
phase: "waiting-device",
|
|
3722
3803
|
verificationUri: start.verification_uri,
|
|
3804
|
+
...verificationUriComplete === void 0 ? {} : { verificationUriComplete },
|
|
3723
3805
|
userCode: start.user_code,
|
|
3724
3806
|
expiresAt: deadline
|
|
3725
3807
|
});
|
|
3726
3808
|
session.notify?.({
|
|
3727
3809
|
message: "Enter this code on the verification page to finish signing in.",
|
|
3728
|
-
url: start.verification_uri,
|
|
3810
|
+
url: verificationUriComplete ?? start.verification_uri,
|
|
3729
3811
|
code: start.user_code
|
|
3730
3812
|
});
|
|
3731
3813
|
let waitMs = intervalSec * 1e3;
|
|
@@ -3829,6 +3911,10 @@ async function beginGithubSignIn(deps, method = "browser") {
|
|
|
3829
3911
|
if (current !== void 0 && (current.phase === "waiting-browser" || current.phase === "waiting-device" || current.phase === "exchanging")) {
|
|
3830
3912
|
throw new Error("already-in-flight");
|
|
3831
3913
|
}
|
|
3914
|
+
const effectiveMethod = resolveMethod(deps, method);
|
|
3915
|
+
if (effectiveMethod !== method) {
|
|
3916
|
+
deps.logger?.("auth.method.fallback", { from: method, to: effectiveMethod, reason: "no-client-secret" });
|
|
3917
|
+
}
|
|
3832
3918
|
const controller = new AbortController();
|
|
3833
3919
|
const timer = setTimeout(() => controller.abort(), attemptTimeout(deps));
|
|
3834
3920
|
const attempt = { phase: "idle", controller, done: Promise.resolve() };
|
|
@@ -3837,13 +3923,13 @@ async function beginGithubSignIn(deps, method = "browser") {
|
|
|
3837
3923
|
try {
|
|
3838
3924
|
started = authorization.begin({
|
|
3839
3925
|
key: GITHUB_RECORD_KEY2,
|
|
3840
|
-
method,
|
|
3926
|
+
method: effectiveMethod,
|
|
3841
3927
|
signal: controller.signal,
|
|
3842
3928
|
// 浏览器往返靠 authorizeUrl + 回调;notify 只更新内存视图,prompt 不支持(拒绝)。
|
|
3843
3929
|
interaction: {
|
|
3844
3930
|
notify: (notice) => {
|
|
3845
3931
|
if (notice.code !== void 0 && notice.url !== void 0) {
|
|
3846
|
-
updateCurrent({ phase: "waiting-device",
|
|
3932
|
+
updateCurrent({ phase: "waiting-device", userCode: notice.code });
|
|
3847
3933
|
} else if (notice.url !== void 0) {
|
|
3848
3934
|
updateCurrent({ phase: "waiting-browser", authorizeUrl: notice.url });
|
|
3849
3935
|
}
|
|
@@ -4455,6 +4541,7 @@ function githubAuthDeps(deps) {
|
|
|
4455
4541
|
get: deps.get,
|
|
4456
4542
|
...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl },
|
|
4457
4543
|
...deps.githubClientId === void 0 ? {} : { clientId: deps.githubClientId },
|
|
4544
|
+
...deps.githubClientSecret === void 0 ? {} : { clientSecret: deps.githubClientSecret },
|
|
4458
4545
|
...deps.logger === void 0 ? {} : { logger: deps.logger }
|
|
4459
4546
|
};
|
|
4460
4547
|
}
|
|
@@ -4794,7 +4881,12 @@ async function statePayload(deps, settings) {
|
|
|
4794
4881
|
/** 浏览器登录 attempt 视图(idle / waiting-browser / waiting-device / exchanging / done / error)。 */
|
|
4795
4882
|
attempt: attemptView(),
|
|
4796
4883
|
/** 浏览器登录是否可用:authorization 服务在、且 GitHub flow 真的注册上了(client_id 有内置默认)。 */
|
|
4797
|
-
browserAvailable: browserSignInAvailable(deps)
|
|
4884
|
+
browserAvailable: browserSignInAvailable(deps),
|
|
4885
|
+
/**
|
|
4886
|
+
* 回环 web flow 是否可用:GitHub 换 token 必须有 client_secret,内置 client_id 没有 → 只能设备码。
|
|
4887
|
+
* 前端据此决定主按钮启动哪个 method(false 时直接用 device,避免点了必然失败的 web flow)。
|
|
4888
|
+
*/
|
|
4889
|
+
loopbackAvailable: isLoopbackSignInAvailable(githubAuthDeps(deps))
|
|
4798
4890
|
}
|
|
4799
4891
|
};
|
|
4800
4892
|
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
createUpdateService,
|
|
10
10
|
ensureGithubFlow,
|
|
11
11
|
readPluginInfo
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-QOYTABJH.js";
|
|
13
13
|
|
|
14
14
|
// src/index.ts
|
|
15
15
|
var name = "dsh-github-trending";
|
|
@@ -20,6 +20,7 @@ function readVersion() {
|
|
|
20
20
|
var VERSION = readVersion();
|
|
21
21
|
function apply(ctx, config = {}) {
|
|
22
22
|
const env = process.env;
|
|
23
|
+
const githubClientSecret = (config.githubClientSecret ?? env.DSH_GITHUB_CLIENT_SECRET)?.trim();
|
|
23
24
|
const get = (key) => typeof ctx.get === "function" ? ctx.get(key) : void 0;
|
|
24
25
|
const log = (event, data) => {
|
|
25
26
|
const suffix = data === void 0 ? "" : ` ${JSON.stringify(data)}`;
|
|
@@ -67,7 +68,8 @@ function apply(ctx, config = {}) {
|
|
|
67
68
|
const githubAuth = (resolve) => ({
|
|
68
69
|
get: resolve,
|
|
69
70
|
logger: log,
|
|
70
|
-
...config.githubClientId === void 0 ? {} : { clientId: config.githubClientId }
|
|
71
|
+
...config.githubClientId === void 0 ? {} : { clientId: config.githubClientId },
|
|
72
|
+
...githubClientSecret === void 0 || githubClientSecret === "" ? {} : { clientSecret: githubClientSecret }
|
|
71
73
|
});
|
|
72
74
|
const registerAuth = (resolve) => {
|
|
73
75
|
if (!ensureGithubFlow(githubAuth(resolve ?? get))) log("auth.flow.deferred", { reason: "authorization-not-ready" });
|
|
@@ -100,6 +102,7 @@ function apply(ctx, config = {}) {
|
|
|
100
102
|
pluginName: pluginInfo.name,
|
|
101
103
|
...pluginInfo.repository === void 0 ? {} : { repository: pluginInfo.repository },
|
|
102
104
|
...config.githubClientId === void 0 ? {} : { githubClientId: config.githubClientId },
|
|
105
|
+
...githubClientSecret === void 0 || githubClientSecret === "" ? {} : { githubClientSecret },
|
|
103
106
|
logger: log
|
|
104
107
|
});
|
|
105
108
|
const register = () => {
|
package/lib/internal.d.ts
CHANGED
|
@@ -1164,7 +1164,13 @@ declare const CALLBACK_PATH = "/api/morehao/github-trending/oauth/github/callbac
|
|
|
1164
1164
|
/**
|
|
1165
1165
|
* 内置的 GitHub OAuth App 公开 client_id(**非机密**:OAuth 的 client_id 设计上就是公开的,
|
|
1166
1166
|
* 会出现在授权 URL 里,与 `gh`/VS Code 内置自己的 client_id 同理)。
|
|
1167
|
-
*
|
|
1167
|
+
*
|
|
1168
|
+
* ⚠️ 只有 client_id 不够:GitHub 的 web application flow 在换 token 时**仍然要求
|
|
1169
|
+
* client_secret**(PKCE 是叠加保护,不能替代,2025-07 起官方也只说「推荐」而非「可免 secret」)。
|
|
1170
|
+
* 因此:
|
|
1171
|
+
* - 内置 client_id → 只能走 **device flow**(`resolveMethod` 自动降级),开箱即用;
|
|
1172
|
+
* - 用户自带 OAuth App(`githubClientId` + `githubClientSecret`)→ 才启用回环 web flow。
|
|
1173
|
+
* 且 device flow 需要在 OAuth App 设置里勾选启用(本插件内置的 App 已启用)。
|
|
1168
1174
|
*
|
|
1169
1175
|
* OAuth App 侧登记的回调:`http://127.0.0.1/api/morehao/github-trending/oauth/github/callback`
|
|
1170
1176
|
* (loopback 按 RFC 8252 忽略端口,运行时随机端口可正常回跳)。
|
|
@@ -1172,6 +1178,18 @@ declare const CALLBACK_PATH = "/api/morehao/github-trending/oauth/github/callbac
|
|
|
1172
1178
|
declare const DEFAULT_GITHUB_CLIENT_ID = "Ov23li7rhp0EKoqbkJKY";
|
|
1173
1179
|
/** 解析生效的 client_id:显式 config 优先,否则用内置默认。 */
|
|
1174
1180
|
declare function resolveClientId(deps: GithubAuthDeps): string | undefined;
|
|
1181
|
+
/** 解析生效的 client_secret(没有就返回 undefined,绝不以空串形式发出去)。 */
|
|
1182
|
+
declare function resolveClientSecret(deps: GithubAuthDeps): string | undefined;
|
|
1183
|
+
/**
|
|
1184
|
+
* 回环(浏览器 web flow)是否可用:GitHub 换 token 必须带 client_secret,
|
|
1185
|
+
* 内置的公开 client_id 没有 secret,只能退到设备码。见文件头的说明。
|
|
1186
|
+
*/
|
|
1187
|
+
declare function isLoopbackSignInAvailable(deps: GithubAuthDeps): boolean;
|
|
1188
|
+
/**
|
|
1189
|
+
* 实际要跑的方法:`browser` 需要 client_secret,缺省时自动降级为 `device`(开箱即用)。
|
|
1190
|
+
* 显式选择 `device` 的调用不受影响。
|
|
1191
|
+
*/
|
|
1192
|
+
declare function resolveMethod(deps: GithubAuthDeps, requested: 'browser' | 'device'): 'browser' | 'device';
|
|
1175
1193
|
/** 本插件运行需要 push / 建 PR,`read:user` 不够;一次授权到 `repo`。 */
|
|
1176
1194
|
declare const DEFAULT_SCOPE = "repo";
|
|
1177
1195
|
interface AuthorizationFlow {
|
|
@@ -1229,6 +1247,11 @@ interface GithubAuthDeps {
|
|
|
1229
1247
|
};
|
|
1230
1248
|
/** GitHub OAuth App 的 public client_id(无需 secret)。 */
|
|
1231
1249
|
clientId?: string;
|
|
1250
|
+
/**
|
|
1251
|
+
* GitHub OAuth App 的 client_secret。只有自带 OAuth App 的回环 web flow 才需要;
|
|
1252
|
+
* 缺省时 `browser` 方法自动降级为 device flow。不进 `settings.json`、不进日志。
|
|
1253
|
+
*/
|
|
1254
|
+
clientSecret?: string;
|
|
1232
1255
|
scope?: string;
|
|
1233
1256
|
attemptTimeoutMs?: number;
|
|
1234
1257
|
}
|
|
@@ -1237,12 +1260,16 @@ interface GithubAttemptView {
|
|
|
1237
1260
|
phase: GithubAttemptPhase;
|
|
1238
1261
|
authorizeUrl?: string;
|
|
1239
1262
|
verificationUri?: string;
|
|
1263
|
+
/** device flow 预填验证码的地址(GitHub 返回的 `verification_uri_complete`):打开只需点授权。 */
|
|
1264
|
+
verificationUriComplete?: string;
|
|
1240
1265
|
userCode?: string;
|
|
1241
1266
|
expiresAt?: number;
|
|
1242
1267
|
login?: string;
|
|
1243
1268
|
error?: string;
|
|
1244
1269
|
}
|
|
1245
1270
|
declare function attemptView(): GithubAttemptView;
|
|
1271
|
+
/** 判断一个未知值是否是可直接打开的 http(s) URL(用于校验 GitHub 返回的验证页地址)。 */
|
|
1272
|
+
declare function isHttpUrl(value: unknown): boolean;
|
|
1246
1273
|
/** PKCE:返回 verifier 与 S256 challenge。 */
|
|
1247
1274
|
declare function createPkce(): {
|
|
1248
1275
|
verifier: string;
|
|
@@ -1250,6 +1277,13 @@ declare function createPkce(): {
|
|
|
1250
1277
|
};
|
|
1251
1278
|
/** 常量时间比较 state(长度不等直接 false,避免 timingSafeEqual 抛错)。 */
|
|
1252
1279
|
declare function verifyState(received: string, expected: string): boolean;
|
|
1280
|
+
/** 回调落地页的两种结果(纯展示,无外部资源,离线可测)。 */
|
|
1281
|
+
type CallbackPageKind = 'success' | 'error';
|
|
1282
|
+
/**
|
|
1283
|
+
* 回环回调的落地页 HTML。自包含、无外链资源(CSP 友好、离线可用),
|
|
1284
|
+
* 跟随系统深浅色;成功页会尝试自动关闭本窗口(脚本失败时文字兜底)。
|
|
1285
|
+
*/
|
|
1286
|
+
declare function renderCallbackPage(kind: CallbackPageKind): string;
|
|
1253
1287
|
/** 拼授权页 URL。 */
|
|
1254
1288
|
declare function buildAuthorizeUrl(params: {
|
|
1255
1289
|
authorizeEndpoint: string;
|
|
@@ -1321,12 +1355,17 @@ declare const githubAuth_cancelGithubSignIn: typeof cancelGithubSignIn;
|
|
|
1321
1355
|
declare const githubAuth_createPkce: typeof createPkce;
|
|
1322
1356
|
declare const githubAuth_ensureGithubFlow: typeof ensureGithubFlow;
|
|
1323
1357
|
declare const githubAuth_isGithubFlowRegistered: typeof isGithubFlowRegistered;
|
|
1358
|
+
declare const githubAuth_isHttpUrl: typeof isHttpUrl;
|
|
1359
|
+
declare const githubAuth_isLoopbackSignInAvailable: typeof isLoopbackSignInAvailable;
|
|
1324
1360
|
declare const githubAuth_registerGithubFlow: typeof registerGithubFlow;
|
|
1361
|
+
declare const githubAuth_renderCallbackPage: typeof renderCallbackPage;
|
|
1325
1362
|
declare const githubAuth_resetAttemptForTest: typeof resetAttemptForTest;
|
|
1326
1363
|
declare const githubAuth_resolveClientId: typeof resolveClientId;
|
|
1364
|
+
declare const githubAuth_resolveClientSecret: typeof resolveClientSecret;
|
|
1365
|
+
declare const githubAuth_resolveMethod: typeof resolveMethod;
|
|
1327
1366
|
declare const githubAuth_verifyState: typeof verifyState;
|
|
1328
1367
|
declare namespace githubAuth {
|
|
1329
|
-
export { type githubAuth_AuthorizationFlow as AuthorizationFlow, type githubAuth_AuthorizationLike as AuthorizationLike, type githubAuth_AuthorizationSessionLike as AuthorizationSessionLike, githubAuth_CALLBACK_PATH as CALLBACK_PATH, githubAuth_DEFAULT_GITHUB_CLIENT_ID as DEFAULT_GITHUB_CLIENT_ID, githubAuth_DEFAULT_SCOPE as DEFAULT_SCOPE, githubAuth_GITHUB_RECORD_KEY as GITHUB_RECORD_KEY, type githubAuth_GithubAttemptPhase as GithubAttemptPhase, type githubAuth_GithubAttemptView as GithubAttemptView, type githubAuth_GithubAuthDeps as GithubAuthDeps, type githubAuth_GithubGrantPayload as GithubGrantPayload, type githubAuth_WebServerLike as WebServerLike, githubAuth_attemptView as attemptView, githubAuth_beginGithubSignIn as beginGithubSignIn, githubAuth_buildAuthorizeUrl as buildAuthorizeUrl, githubAuth_cancelGithubSignIn as cancelGithubSignIn, githubAuth_createPkce as createPkce, githubAuth_ensureGithubFlow as ensureGithubFlow, githubAuth_isGithubFlowRegistered as isGithubFlowRegistered, githubAuth_registerGithubFlow as registerGithubFlow, githubAuth_resetAttemptForTest as resetAttemptForTest, githubAuth_resolveClientId as resolveClientId, githubAuth_verifyState as verifyState };
|
|
1368
|
+
export { type githubAuth_AuthorizationFlow as AuthorizationFlow, type githubAuth_AuthorizationLike as AuthorizationLike, type githubAuth_AuthorizationSessionLike as AuthorizationSessionLike, githubAuth_CALLBACK_PATH as CALLBACK_PATH, githubAuth_DEFAULT_GITHUB_CLIENT_ID as DEFAULT_GITHUB_CLIENT_ID, githubAuth_DEFAULT_SCOPE as DEFAULT_SCOPE, githubAuth_GITHUB_RECORD_KEY as GITHUB_RECORD_KEY, type githubAuth_GithubAttemptPhase as GithubAttemptPhase, type githubAuth_GithubAttemptView as GithubAttemptView, type githubAuth_GithubAuthDeps as GithubAuthDeps, type githubAuth_GithubGrantPayload as GithubGrantPayload, type githubAuth_WebServerLike as WebServerLike, githubAuth_attemptView as attemptView, githubAuth_beginGithubSignIn as beginGithubSignIn, githubAuth_buildAuthorizeUrl as buildAuthorizeUrl, githubAuth_cancelGithubSignIn as cancelGithubSignIn, githubAuth_createPkce as createPkce, githubAuth_ensureGithubFlow as ensureGithubFlow, githubAuth_isGithubFlowRegistered as isGithubFlowRegistered, githubAuth_isHttpUrl as isHttpUrl, githubAuth_isLoopbackSignInAvailable as isLoopbackSignInAvailable, githubAuth_registerGithubFlow as registerGithubFlow, githubAuth_renderCallbackPage as renderCallbackPage, githubAuth_resetAttemptForTest as resetAttemptForTest, githubAuth_resolveClientId as resolveClientId, githubAuth_resolveClientSecret as resolveClientSecret, githubAuth_resolveMethod as resolveMethod, githubAuth_verifyState as verifyState };
|
|
1330
1369
|
}
|
|
1331
1370
|
|
|
1332
1371
|
interface DefaultBranchResult {
|
|
@@ -2010,8 +2049,10 @@ interface RouteDeps {
|
|
|
2010
2049
|
/** 包名与仓库地址都来自 package.json,由 src/index.ts 传入。 */
|
|
2011
2050
|
pluginName?: string;
|
|
2012
2051
|
repository?: string;
|
|
2013
|
-
/** 浏览器 GitHub 登录用的 public client_id(由 apply 的 config
|
|
2052
|
+
/** 浏览器 GitHub 登录用的 public client_id(由 apply 的 config 传入;缺省时用内置默认)。 */
|
|
2014
2053
|
githubClientId?: string;
|
|
2054
|
+
/** 自带 OAuth App 时的 client_secret(只用于回环 web flow;缺省时该 method 自动降级为设备码)。 */
|
|
2055
|
+
githubClientSecret?: string;
|
|
2015
2056
|
logger?: (event: string, data?: Record<string, unknown>) => void;
|
|
2016
2057
|
}
|
|
2017
2058
|
declare function createRoutes(deps: RouteDeps): (request: http.IncomingMessage, response: http.ServerResponse) => Promise<void>;
|
package/lib/internal.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morehao/dsh-github-trending",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "GitHub 趋势日报的 DeepSeek Harness 插件:按日 / 周 / 月周期定时 spawn 本地 trendradar 核心,抓取 Trending、生成中文简介与周期总结,并可选把订阅仓库的新 Release 合成中文摘要,一起提交到你的仓库、自动开 PR 合并;内置概览 / 榜单 / 运行记录 / 订阅 / 设置工作台。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|