@morehao/dsh-github-trending 0.8.4 → 0.8.6

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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.8.6] - 2026-11-02
4
+
5
+ ### Features
6
+
7
+ - **auth**: device-code sign-in modal with prominent code (#82)
8
+
9
+ ### Refactors
10
+
11
+ - **auth**: drop loopback web flow, keep device flow only (#81)
12
+
13
+ **Full Changelog**: https://github.com/morehao/dsh-github-trending/compare/v0.8.5...v0.8.6
14
+
15
+ ## [v0.8.5] - 2026-11-02
16
+
17
+ ### Fixes
18
+
19
+ - **auth**: show device code with one-click copy
20
+
21
+ **Full Changelog**: https://github.com/morehao/dsh-github-trending/compare/v0.8.4...v0.8.5
22
+
3
23
  ## [v0.8.4] - 2026-11-02
4
24
 
5
25
  ### Features
package/README.md CHANGED
@@ -59,7 +59,7 @@ GitHub 趋势榜的 **DeepSeek Harness 插件**:按日 / 周 / 月周期定时
59
59
 
60
60
  认证分区支持三种方式(可随时切换):
61
61
 
62
- - **浏览器登录(默认推荐)**:点「用 GitHub 登录」在浏览器完成授权,无需手工粘贴 token,token 由 DSH 凭据记录安全保存、不落 `settings.json`。插件内置了公开的 OAuth App client_id,开箱即用;GitHub 的 web flow 换 token 仍然**必须带 client_secret**(PKCE 只是叠加保护),而客户端不能内置机密,所以默认走**设备码**:点一下后在打开的 GitHub 页面输入验证码即可。若你自带 OAuth App,可在插件配置里同时给 `githubClientId` + `githubClientSecret`,就会改用本机回环回调(不用手输验证码)。
62
+ - **浏览器登录(推荐)**:点「用 GitHub 登录」在浏览器完成授权,无需手工粘贴 token,token 由 DSH 凭据记录安全保存、不落 `settings.json`。插件内置了公开的 OAuth App client_id,开箱即用。GitHub 的 web flow 换 token 必须带 client_secret(客户端不能内置机密),所以走**设备码**:点一下打开 GitHub 验证页,把页面上的验证码(设置页可一键复制)输入并确认即可。
63
63
  - **本机 gh**:复用你终端里 `gh auth login` 的登录态。
64
64
  - **PAT**:手工填 Personal Access Token(存到凭据引用 / 环境变量)。
65
65
 
package/client.js CHANGED
@@ -125,18 +125,24 @@ 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 安全保存,无需手工粘贴。点一下会打开 GitHub 验证页,把下方验证码输入并确认即可。",
129
129
  "auth.browser.signin": "用 GitHub 登录",
130
- "auth.browser.waiting": "等待浏览器授权…",
131
- "auth.browser.code": "如未自动填入,请手动输入验证码:",
130
+ "auth.browser.copyCode": "复制验证码",
131
+ "auth.browser.codeCopied": "验证码已复制",
132
+ "auth.browser.codeCopyFailed": "复制失败,请手动记下验证码",
132
133
  "auth.browser.cancel": "取消",
133
134
  "auth.browser.done": "已登录",
134
- "auth.browser.device": "用设备码登录",
135
135
  "auth.browser.unavailable": "宿主的 authorization 服务不可用(或 GitHub 授权流程没注册成功):请重试一次;仍不行就改用「本机 gh」或 PAT。",
136
- "auth.browser.openHint": "若浏览器没有自动打开,点下面的链接完成授权,或复制链接:",
137
- "auth.browser.open": "打开授权页",
138
- "auth.browser.copy": "复制链接",
139
- "auth.browser.copied": "授权链接已复制",
136
+ "auth.modal.title": "用 GitHub 登录",
137
+ "auth.modal.lead": "在浏览器中打开 GitHub,输入下面的验证码完成授权。",
138
+ "auth.modal.openPage": "打开 GitHub 验证页",
139
+ "auth.modal.waiting": "等待授权中…",
140
+ "auth.modal.exchanging": "授权成功,正在换取令牌…",
141
+ "auth.modal.requesting": "正在向 GitHub 申请验证码…",
142
+ "auth.modal.retry": "重试",
143
+ "auth.modal.close": "关闭",
144
+ "auth.modal.closeHint": "关窗不取消,可随时回来查看。",
145
+ "auth.modal.viewCode": "查看验证码",
140
146
  "auth.browser.failed": "登录失败",
141
147
  "auth.browser.failed.detail": "登录失败:{message}",
142
148
  "auth.browser.failed.hint": "请重试一次;仍失败可改用「本机 gh」或 PAT。",
@@ -670,18 +676,24 @@ var DICT = {
670
676
  "auth.gh": "Local gh",
671
677
  "auth.browser": "Browser login",
672
678
  "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. Clicking opens the authorization page with the code usually pre-filled — just confirm.",
679
+ "auth.browser.hint": "Authorize GitHub in your browser; DSH stores the token securely, no manual paste. Clicking opens the GitHub verification page — enter the code below and confirm.",
674
680
  "auth.browser.signin": "Sign in with GitHub",
675
- "auth.browser.waiting": "Waiting for browser authorization…",
676
- "auth.browser.code": "If it is not filled in automatically, enter this code:",
681
+ "auth.browser.copyCode": "Copy code",
682
+ "auth.browser.codeCopied": "Code copied",
683
+ "auth.browser.codeCopyFailed": "Copy failed — please note the code manually",
677
684
  "auth.browser.cancel": "Cancel",
678
685
  "auth.browser.done": "Signed in",
679
- "auth.browser.device": "Use a device code",
680
686
  "auth.browser.unavailable": "The host authorization service is unavailable (or the GitHub flow did not register): retry once, then fall back to Local gh or a PAT.",
681
- "auth.browser.openHint": "If the browser did not open, use the link below to finish signing in, or copy the link:",
682
- "auth.browser.open": "Open authorization page",
683
- "auth.browser.copy": "Copy link",
684
- "auth.browser.copied": "Authorization link copied",
687
+ "auth.modal.title": "Sign in with GitHub",
688
+ "auth.modal.lead": "Open GitHub in your browser and enter the code below to authorize.",
689
+ "auth.modal.openPage": "Open GitHub verification page",
690
+ "auth.modal.waiting": "Waiting for authorization…",
691
+ "auth.modal.exchanging": "Authorized — exchanging for token…",
692
+ "auth.modal.requesting": "Requesting a device code from GitHub…",
693
+ "auth.modal.retry": "Retry",
694
+ "auth.modal.close": "Close",
695
+ "auth.modal.closeHint": "Closing keeps the attempt running; reopen anytime.",
696
+ "auth.modal.viewCode": "View code",
685
697
  "auth.browser.failed": "Sign-in failed",
686
698
  "auth.browser.failed.detail": "Sign-in failed: {message}",
687
699
  "auth.browser.failed.hint": "Retry once; if it keeps failing, use Local gh or a PAT instead.",
@@ -1232,6 +1244,20 @@ var CSS = `
1232
1244
  .dshgt-modal-mask{position:fixed;inset:0;background:var(--dsw-alias-bg-mask-1,rgba(15,23,42,.38));display:flex;align-items:center;justify-content:center;z-index:2147483000;}
1233
1245
  .dshgt-modal{background:var(--gt-bg);border-radius:12px;border:1px solid var(--gt-border);max-width:640px;width:calc(100% - 32px);max-height:80vh;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:10px;box-shadow:0 20px 50px rgba(15,23,42,.25);}
1234
1246
 
1247
+ /* ——— GitHub 设备码登录弹窗 ——— */
1248
+ .dshgt-authmodal{max-width:420px;gap:14px;}
1249
+ .dshgt-authmodal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;}
1250
+ .dshgt-authmodal-x{border:0;background:transparent;color:var(--gt-muted);font-size:14px;line-height:1;cursor:pointer;padding:4px 6px;border-radius:6px;}
1251
+ .dshgt-authmodal-x:hover{background:color-mix(in srgb,var(--gt-fg) 8%,transparent);color:var(--gt-fg);}
1252
+ .dshgt-authmodal-body{display:flex;flex-direction:column;gap:12px;}
1253
+ .dshgt-authcode{display:flex;align-items:center;justify-content:center;padding:16px 12px;border:1px dashed color-mix(in srgb,var(--gt-accent) 45%,var(--gt-border));border-radius:12px;background:color-mix(in srgb,var(--gt-accent) 6%,var(--gt-bg));}
1254
+ .dshgt-authcode-value{font-family:var(--gt-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:30px;font-weight:700;letter-spacing:6px;line-height:1.1;color:var(--gt-fg);user-select:all;}
1255
+ .dshgt-authcode-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;}
1256
+ .dshgt-authwait{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--gt-muted);}
1257
+ .dshgt-authwait-dot{width:8px;height:8px;border-radius:50%;background:var(--gt-accent);animation:dshgt-pulse 1.1s ease-in-out infinite;}
1258
+ @keyframes dshgt-pulse{0%,100%{opacity:.25;transform:scale(.8);}50%{opacity:1;transform:scale(1);}}
1259
+ .dshgt-authmodal-footer{display:flex;align-items:center;justify-content:space-between;gap:8px;border-top:1px solid var(--gt-border);padding-top:10px;}
1260
+
1235
1261
  /* ——— 01 工作台外壳 ——— */
1236
1262
  .dshgt-shell{gap:0;height:100%;min-height:0;overflow:hidden;}
1237
1263
  .dshgt-app{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--gt-bg);}
@@ -1885,8 +1911,8 @@ async function apiCall(method, path, body, options = {}) {
1885
1911
  }
1886
1912
  return json;
1887
1913
  }
1888
- async function beginGithubSignIn(method = "browser") {
1889
- const payload = await apiCall("POST", "/github/browser/begin", { method });
1914
+ async function beginGithubSignIn() {
1915
+ const payload = await apiCall("POST", "/github/browser/begin", {});
1890
1916
  return payload.attempt;
1891
1917
  }
1892
1918
  async function getGithubSignInStatus(signal) {
@@ -2130,6 +2156,7 @@ function SettingsView(props) {
2130
2156
  const [testing, setTesting] = import_react.default.useState(false);
2131
2157
  const [githubTest, setGithubTest] = import_react.default.useState(null);
2132
2158
  const [signIn, setSignIn] = import_react.default.useState({ phase: "idle" });
2159
+ const [signInModal, setSignInModal] = import_react.default.useState(false);
2133
2160
  const anyDirty = SECTIONS.some((id) => dirtySections[id]);
2134
2161
  const sectionEditable = section !== "source";
2135
2162
  import_react.default.useEffect(() => {
@@ -2257,44 +2284,43 @@ function SettingsView(props) {
2257
2284
  setTesting(false);
2258
2285
  }
2259
2286
  };
2260
- const startSignIn = async (method) => {
2287
+ const openVerifyPage = (url) => {
2288
+ if (url === void 0) return;
2289
+ try {
2290
+ window.open(url, "_blank", "noopener,noreferrer");
2291
+ } catch {
2292
+ }
2293
+ };
2294
+ const startSignIn = async () => {
2295
+ setSignInModal(true);
2296
+ setSignIn({ phase: "idle" });
2261
2297
  try {
2262
- let view = await beginGithubSignIn(method);
2298
+ let view = await beginGithubSignIn();
2263
2299
  if (view.phase === "idle") view = await getGithubSignInStatus();
2264
2300
  setSignIn(view);
2265
- if (view.phase === "error") {
2266
- notify("err", t("auth.browser.failed.detail", { message: view.error ?? "" }));
2267
- return;
2268
- }
2269
- if (view.phase === "waiting-browser" && view.authorizeUrl !== void 0) {
2270
- try {
2271
- window.open(view.authorizeUrl, "_blank", "noopener,noreferrer");
2272
- } catch {
2273
- }
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
- }
2301
+ if (view.phase === "waiting-device") openVerifyPage(view.verificationUri);
2282
2302
  } catch (error) {
2283
- notify("err", error instanceof Error ? error.message : String(error));
2303
+ setSignIn({ phase: "error", error: error instanceof Error ? error.message : String(error) });
2284
2304
  }
2285
2305
  };
2306
+ const openSignInModal = () => setSignInModal(true);
2307
+ const closeSignInModal = () => setSignInModal(false);
2286
2308
  const cancelSignIn = async () => {
2287
2309
  try {
2288
2310
  setSignIn(await cancelGithubSignIn());
2289
2311
  } catch (error) {
2290
2312
  notify("err", error instanceof Error ? error.message : String(error));
2313
+ } finally {
2314
+ setSignInModal(false);
2291
2315
  }
2292
2316
  };
2293
- const copyAuthorizeLink = () => {
2294
- if (signIn.authorizeUrl === void 0) return;
2295
- void navigator.clipboard?.writeText(signIn.authorizeUrl).then(() => notify("ok", t("auth.browser.copied")));
2317
+ const copyDeviceCode = () => {
2318
+ if (signIn.userCode === void 0) return;
2319
+ void copyText(signIn.userCode).then(
2320
+ (ok) => notify(ok ? "ok" : "err", ok ? t("auth.browser.codeCopied") : t("auth.browser.codeCopyFailed"))
2321
+ );
2296
2322
  };
2297
- const signInActive = signIn.phase === "waiting-browser" || signIn.phase === "waiting-device" || signIn.phase === "exchanging";
2323
+ const signInActive = signIn.phase === "waiting-device" || signIn.phase === "exchanging";
2298
2324
  import_react.default.useEffect(() => {
2299
2325
  if (!signInActive) return void 0;
2300
2326
  let alive = true;
@@ -2355,7 +2381,6 @@ function SettingsView(props) {
2355
2381
  const isGh = draft.github.authMode === "gh";
2356
2382
  const isBrowser = draft.github.authMode === "browser";
2357
2383
  const browserAvailable = state.github.browserAvailable === true;
2358
- const loopbackAvailable = state.github.loopbackAvailable === true;
2359
2384
  const sections = [
2360
2385
  { id: "source", label: t("st.source") },
2361
2386
  { id: "model", label: t("st.model") },
@@ -2550,13 +2575,10 @@ function SettingsView(props) {
2550
2575
  {
2551
2576
  small: true,
2552
2577
  variant: "primary",
2553
- onClick: () => void startSignIn(loopbackAvailable ? "browser" : "device"),
2554
- disabled: signInActive
2578
+ onClick: () => void (signInActive ? openSignInModal() : startSignIn())
2555
2579
  },
2556
- signIn.phase === "done" && signIn.login !== void 0 ? `${t("auth.browser.done")} · @${signIn.login}` : signInActive ? t("auth.browser.waiting") : t("auth.browser.signin")
2557
- ) : h("span", { className: "dshgt-hint" }, t("auth.browser.unavailable")),
2558
- browserAvailable && loopbackAvailable ? h(Btn, { small: true, variant: "ghost", onClick: () => void startSignIn("device"), disabled: signInActive }, t("auth.browser.device")) : null,
2559
- signInActive ? h(Btn, { small: true, variant: "ghost", onClick: () => void cancelSignIn() }, t("auth.browser.cancel")) : null
2580
+ signInActive ? t("auth.modal.viewCode") : signIn.phase === "done" && signIn.login !== void 0 ? `${t("auth.browser.done")} · @${signIn.login}` : t("auth.browser.signin")
2581
+ ) : h("span", { className: "dshgt-hint" }, t("auth.browser.unavailable"))
2560
2582
  ) : h(
2561
2583
  Row,
2562
2584
  { label: t("auth.tokenRef") },
@@ -2565,41 +2587,6 @@ function SettingsView(props) {
2565
2587
  }) }),
2566
2588
  h("span", { className: "dshgt-hint" }, t("auth.tokenRef.hint"))
2567
2589
  ),
2568
- // 浏览器登录失败:必须显式说出来,否则「点了没反应」又回来了。
2569
- isBrowser && signIn.phase === "error" ? h(Alert, { kind: "err", title: t("auth.browser.failed") }, signIn.error ?? t("auth.browser.failed.hint")) : null,
2570
- // 浏览器登录进行中的状态行:可点的授权页链接 + 复制兜底 + 设备码。
2571
- isBrowser && (signIn.phase === "waiting-browser" || signIn.phase === "waiting-device" || signIn.phase === "exchanging") ? h(
2572
- "div",
2573
- { className: "dshgt-hint" },
2574
- signIn.authorizeUrl !== void 0 ? h(
2575
- "div",
2576
- { className: "dshgt-desc" },
2577
- h("span", null, t("auth.browser.openHint")),
2578
- " ",
2579
- h(
2580
- "a",
2581
- { className: "dshgt-link", href: signIn.authorizeUrl, target: "_blank", rel: "noreferrer" },
2582
- t("auth.browser.open")
2583
- ),
2584
- " ",
2585
- h(Btn, { small: true, variant: "ghost", onClick: () => copyAuthorizeLink() }, t("auth.browser.copy"))
2586
- ) : null,
2587
- signIn.verificationUriComplete !== void 0 || signIn.verificationUri !== void 0 ? h(
2588
- "div",
2589
- { className: "dshgt-desc" },
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
- )
2600
- ) : null,
2601
- signIn.userCode !== void 0 ? h("div", { className: "dshgt-mono" }, `${t("auth.browser.code")} ${signIn.userCode}`) : null
2602
- ) : null,
2603
2590
  h(
2604
2591
  Row,
2605
2592
  { label: t("auth.test") },
@@ -2873,7 +2860,88 @@ function SettingsView(props) {
2873
2860
  setDirtySections({ ...EMPTY_DIRTY });
2874
2861
  setSavedSection(null);
2875
2862
  void store.refresh({ quiet: true });
2876
- } }) : null
2863
+ } }) : null,
2864
+ signInModal ? h(AuthSignInModal, {
2865
+ t,
2866
+ view: signIn,
2867
+ onClose: closeSignInModal,
2868
+ onCancel: () => void cancelSignIn(),
2869
+ onRetry: () => void startSignIn(),
2870
+ onCopyCode: copyDeviceCode,
2871
+ onOpenPage: () => openVerifyPage(signIn.verificationUri)
2872
+ }) : null
2873
+ );
2874
+ }
2875
+ function AuthSignInModal(props) {
2876
+ const { t, view } = props;
2877
+ const active = view.phase === "waiting-device" || view.phase === "exchanging";
2878
+ const body = view.phase === "waiting-device" || view.phase === "exchanging" ? h(
2879
+ "div",
2880
+ { className: "dshgt-authmodal-body" },
2881
+ h("p", { className: "dshgt-hint", style: { margin: 0 } }, t("auth.modal.lead")),
2882
+ view.userCode !== void 0 ? h(
2883
+ "div",
2884
+ { className: "dshgt-authcode" },
2885
+ h("span", { className: "dshgt-authcode-value" }, view.userCode)
2886
+ ) : null,
2887
+ view.userCode !== void 0 ? h(
2888
+ "div",
2889
+ { className: "dshgt-authcode-actions" },
2890
+ h(Btn, { small: true, variant: "ghost", onClick: props.onCopyCode }, t("auth.browser.copyCode")),
2891
+ view.verificationUri !== void 0 ? h(Btn, { small: true, variant: "primary", onClick: props.onOpenPage }, `${t("auth.modal.openPage")} ↗`) : null
2892
+ ) : null,
2893
+ h(
2894
+ "div",
2895
+ { className: "dshgt-authwait" },
2896
+ h("span", { className: "dshgt-authwait-dot" }),
2897
+ h("span", null, view.phase === "exchanging" ? t("auth.modal.exchanging") : t("auth.modal.waiting"))
2898
+ )
2899
+ ) : view.phase === "done" ? h(
2900
+ "div",
2901
+ { className: "dshgt-authmodal-body" },
2902
+ h(Alert, { kind: "ok" }, view.login === void 0 ? t("auth.browser.done") : `${t("auth.browser.done")} · @${view.login}`)
2903
+ ) : view.phase === "error" ? h(
2904
+ "div",
2905
+ { className: "dshgt-authmodal-body" },
2906
+ h(Alert, { kind: "err", title: t("auth.browser.failed") }, view.error ?? t("auth.browser.failed.hint"))
2907
+ ) : h(
2908
+ "div",
2909
+ { className: "dshgt-authmodal-body dshgt-authwait" },
2910
+ h("span", { className: "dshgt-authwait-dot" }),
2911
+ h("span", null, t("auth.modal.requesting"))
2912
+ );
2913
+ const footer = view.phase === "error" ? h(
2914
+ "div",
2915
+ { className: "dshgt-authmodal-footer" },
2916
+ h(Btn, { small: true, variant: "primary", onClick: props.onRetry }, t("auth.modal.retry")),
2917
+ h(Btn, { small: true, variant: "ghost", onClick: props.onClose }, t("auth.modal.close"))
2918
+ ) : view.phase === "done" ? h(
2919
+ "div",
2920
+ { className: "dshgt-authmodal-footer" },
2921
+ h(Btn, { small: true, variant: "primary", onClick: props.onClose }, t("auth.modal.close"))
2922
+ ) : h(
2923
+ "div",
2924
+ { className: "dshgt-authmodal-footer" },
2925
+ h("span", { className: "dshgt-hint" }, t("auth.modal.closeHint")),
2926
+ h(Btn, { small: true, variant: "ghost", onClick: props.onCancel, disabled: !active }, t("auth.browser.cancel"))
2927
+ );
2928
+ return h(
2929
+ "div",
2930
+ { className: "dshgt-modal-mask", onClick: (event) => {
2931
+ if (event.target === event.currentTarget) props.onClose();
2932
+ } },
2933
+ h(
2934
+ "div",
2935
+ { className: "dshgt-modal dshgt-authmodal", role: "dialog", "aria-modal": "true", "aria-label": t("auth.modal.title") },
2936
+ h(
2937
+ "div",
2938
+ { className: "dshgt-authmodal-head" },
2939
+ h("h3", { style: { margin: 0, fontSize: 14 } }, t("auth.modal.title")),
2940
+ h("button", { type: "button", className: "dshgt-authmodal-x", "aria-label": t("auth.modal.close"), onClick: props.onClose }, "✕")
2941
+ ),
2942
+ body,
2943
+ footer
2944
+ )
2877
2945
  );
2878
2946
  }
2879
2947
  function QuickFillModal(props) {
@@ -3529,48 +3529,26 @@ async function probeGithubAuth(options) {
3529
3529
  // src/host/github-auth.ts
3530
3530
  var github_auth_exports = {};
3531
3531
  __export(github_auth_exports, {
3532
- CALLBACK_PATH: () => CALLBACK_PATH,
3533
3532
  DEFAULT_GITHUB_CLIENT_ID: () => DEFAULT_GITHUB_CLIENT_ID,
3534
3533
  DEFAULT_SCOPE: () => DEFAULT_SCOPE,
3535
3534
  GITHUB_RECORD_KEY: () => GITHUB_RECORD_KEY2,
3536
3535
  attemptView: () => attemptView,
3537
3536
  beginGithubSignIn: () => beginGithubSignIn,
3538
- buildAuthorizeUrl: () => buildAuthorizeUrl,
3539
3537
  cancelGithubSignIn: () => cancelGithubSignIn,
3540
- createPkce: () => createPkce,
3541
3538
  ensureGithubFlow: () => ensureGithubFlow,
3542
3539
  isGithubFlowRegistered: () => isGithubFlowRegistered,
3543
- isHttpUrl: () => isHttpUrl,
3544
- isLoopbackSignInAvailable: () => isLoopbackSignInAvailable,
3545
3540
  registerGithubFlow: () => registerGithubFlow,
3546
- renderCallbackPage: () => renderCallbackPage,
3547
3541
  resetAttemptForTest: () => resetAttemptForTest,
3548
- resolveClientId: () => resolveClientId,
3549
- resolveClientSecret: () => resolveClientSecret,
3550
- resolveMethod: () => resolveMethod,
3551
- verifyState: () => verifyState
3542
+ resolveClientId: () => resolveClientId
3552
3543
  });
3553
- import { createHash as createHash3, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
3554
3544
  var GITHUB_RECORD_KEY2 = "github/default";
3555
- var CALLBACK_PATH = "/api/morehao/github-trending/oauth/github/callback";
3556
3545
  var DEFAULT_GITHUB_CLIENT_ID = "Ov23li7rhp0EKoqbkJKY";
3557
3546
  function resolveClientId(deps) {
3558
3547
  const fromConfig = deps.clientId;
3559
3548
  return fromConfig !== void 0 && fromConfig !== "" ? fromConfig : DEFAULT_GITHUB_CLIENT_ID;
3560
3549
  }
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
- }
3571
- var AUTHORIZE_URL = "https://github.com/login/oauth/authorize";
3572
- var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
3573
3550
  var DEVICE_CODE_URL = "https://github.com/login/device/code";
3551
+ var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
3574
3552
  var GITHUB_USER_URL = "https://api.github.com/user";
3575
3553
  var DEFAULT_SCOPE = "repo";
3576
3554
  var DEFAULT_ATTEMPT_TIMEOUT_MS = 10 * 60 * 1e3;
@@ -3580,105 +3558,6 @@ function attemptView() {
3580
3558
  const { controller: _c, done: _d, ...view } = current;
3581
3559
  return view;
3582
3560
  }
3583
- function base64url(buf) {
3584
- return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
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
- }
3596
- function createPkce() {
3597
- const verifier = base64url(randomBytes2(32));
3598
- const challenge = base64url(createHash3("sha256").update(verifier).digest());
3599
- return { verifier, challenge };
3600
- }
3601
- function verifyState(received, expected) {
3602
- const a = Buffer.from(received);
3603
- const b = Buffer.from(expected);
3604
- return a.length === b.length && timingSafeEqual2(a, b);
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
- }
3641
- function buildAuthorizeUrl(params) {
3642
- const url = new URL(params.authorizeEndpoint);
3643
- url.searchParams.set("client_id", params.clientId);
3644
- url.searchParams.set("redirect_uri", params.redirectUri);
3645
- url.searchParams.set("scope", params.scope);
3646
- url.searchParams.set("state", params.state);
3647
- url.searchParams.set("code_challenge", params.challenge);
3648
- url.searchParams.set("code_challenge_method", "S256");
3649
- return url.toString();
3650
- }
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);
3659
- const response = await fetchImpl(endpoint, {
3660
- method: "POST",
3661
- headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
3662
- body: form.toString(),
3663
- signal
3664
- });
3665
- if (!response.ok) throw new Error(`GitHub access_token HTTP ${response.status}`);
3666
- const body = await response.json();
3667
- if (typeof body.access_token !== "string" || body.access_token === "") {
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`}`);
3675
- }
3676
- return {
3677
- token: body.access_token,
3678
- ...typeof body.token_type === "string" ? { tokenType: body.token_type } : {},
3679
- ...typeof body.scope === "string" ? { scope: body.scope } : {}
3680
- };
3681
- }
3682
3561
  async function fetchLogin(fetchImpl, endpoint, token, signal) {
3683
3562
  try {
3684
3563
  const response = await fetchImpl(endpoint, {
@@ -3692,87 +3571,6 @@ async function fetchLogin(fetchImpl, endpoint, token, signal) {
3692
3571
  return void 0;
3693
3572
  }
3694
3573
  }
3695
- async function runBrowser(deps, session) {
3696
- const webServer = deps.get("webServer");
3697
- if (webServer === void 0 || typeof webServer.register !== "function") {
3698
- throw new Error("webServer \u670D\u52A1\u4E0D\u53EF\u7528\uFF1A\u6D4F\u89C8\u5668\u767B\u5F55\u9700\u8981\u5B83\u627F\u8F7D OAuth \u56DE\u8C03");
3699
- }
3700
- const clientId = resolveClientId(deps);
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
- }
3706
- const fetchImpl = deps.fetchImpl ?? fetch;
3707
- const endpoints = normalizeEndpoints(deps.endpoints);
3708
- const scope = deps.scope ?? DEFAULT_SCOPE;
3709
- const state = base64url(randomBytes2(32));
3710
- const { verifier, challenge } = createPkce();
3711
- let resolveCode;
3712
- let rejectCode;
3713
- const codePromise = new Promise((resolve, reject) => {
3714
- resolveCode = resolve;
3715
- rejectCode = reject;
3716
- });
3717
- codePromise.catch(() => void 0);
3718
- const dispose = webServer.register({
3719
- kind: "exact",
3720
- path: CALLBACK_PATH,
3721
- handler: (req, res) => {
3722
- let url;
3723
- try {
3724
- url = new URL(req.url ?? "/", "http://127.0.0.1");
3725
- } catch {
3726
- res.writeHead(400, { "cache-control": "no-store" }).end();
3727
- return;
3728
- }
3729
- const code = url.searchParams.get("code") ?? "";
3730
- const gotState = url.searchParams.get("state") ?? "";
3731
- const single = url.searchParams.getAll("state").length === 1 && url.searchParams.getAll("code").length === 1;
3732
- const bad = req.method !== "GET" || url.pathname !== CALLBACK_PATH || !single || code === "" || !verifyState(gotState, state);
3733
- if (bad) {
3734
- res.writeHead(400, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(renderCallbackPage("error"));
3735
- return;
3736
- }
3737
- res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(renderCallbackPage("success"));
3738
- resolveCode(code);
3739
- }
3740
- });
3741
- try {
3742
- const origin = localOrigin(webServer);
3743
- const redirectUri = `${origin}${CALLBACK_PATH}`;
3744
- const authorizeUrl = buildAuthorizeUrl({
3745
- authorizeEndpoint: endpoints.authorize,
3746
- clientId,
3747
- redirectUri,
3748
- scope,
3749
- state,
3750
- challenge
3751
- });
3752
- updateCurrent({ phase: "waiting-browser", authorizeUrl, expiresAt: Date.now() + attemptTimeout(deps) });
3753
- session.notify?.({ message: "Continue in your browser to sign in to GitHub.", url: authorizeUrl });
3754
- const code = await withAbort(codePromise, session.signal, () => rejectCode(new Error("authorization cancelled")));
3755
- updateCurrent({ phase: "exchanging" });
3756
- const exchanged = await exchangeCode(
3757
- fetchImpl,
3758
- endpoints.accessToken,
3759
- { clientId, clientSecret, code, verifier, redirectUri },
3760
- session.signal
3761
- );
3762
- const login = await fetchLogin(fetchImpl, endpoints.user, exchanged.token, session.signal);
3763
- const payload = {
3764
- version: 1,
3765
- token: exchanged.token,
3766
- ...login === void 0 ? {} : { login },
3767
- tokenType: exchanged.tokenType ?? "bearer",
3768
- scope: exchanged.scope ?? scope
3769
- };
3770
- if (typeof session.commit === "function") await session.commit({ kind: "grant", payload });
3771
- updateCurrent({ phase: "done", ...login === void 0 ? {} : { login } });
3772
- } finally {
3773
- dispose();
3774
- }
3775
- }
3776
3574
  async function runDevice(deps, session) {
3777
3575
  const clientId = resolveClientId(deps);
3778
3576
  if (clientId === void 0 || clientId === "") throw new Error("\u7F3A\u5C11 GitHub OAuth client_id");
@@ -3797,17 +3595,15 @@ async function runDevice(deps, session) {
3797
3595
  const intervalSec = typeof start.interval === "number" && start.interval > 0 ? start.interval : 5;
3798
3596
  const expiresInSec = typeof start.expires_in === "number" && start.expires_in > 0 ? start.expires_in : 900;
3799
3597
  const deadline = Date.now() + expiresInSec * 1e3;
3800
- const verificationUriComplete = isHttpUrl(start.verification_uri_complete) ? start.verification_uri_complete : void 0;
3801
3598
  updateCurrent({
3802
3599
  phase: "waiting-device",
3803
3600
  verificationUri: start.verification_uri,
3804
- ...verificationUriComplete === void 0 ? {} : { verificationUriComplete },
3805
3601
  userCode: start.user_code,
3806
3602
  expiresAt: deadline
3807
3603
  });
3808
3604
  session.notify?.({
3809
3605
  message: "Enter this code on the verification page to finish signing in.",
3810
- url: verificationUriComplete ?? start.verification_uri,
3606
+ url: start.verification_uri,
3811
3607
  code: start.user_code
3812
3608
  });
3813
3609
  let waitMs = intervalSec * 1e3;
@@ -3852,11 +3648,8 @@ function flowDefinition(deps) {
3852
3648
  return {
3853
3649
  key: GITHUB_RECORD_KEY2,
3854
3650
  label: "GitHub",
3855
- methods: [
3856
- { id: "browser", label: "Sign in with GitHub" },
3857
- { id: "device", label: "Use a device code" }
3858
- ],
3859
- run: (session) => session.method === "device" ? runDevice(deps, session) : runBrowser(deps, session)
3651
+ methods: [{ id: "device", label: "Use a device code" }],
3652
+ run: (session) => runDevice(deps, session)
3860
3653
  };
3861
3654
  }
3862
3655
  var registeredOn = /* @__PURE__ */ new WeakSet();
@@ -3900,21 +3693,17 @@ function registerGithubFlow(deps) {
3900
3693
  }
3901
3694
  var START_TIMEOUT_MS = 1500;
3902
3695
  var START_POLL_MS = 10;
3903
- async function beginGithubSignIn(deps, method = "browser") {
3696
+ async function beginGithubSignIn(deps) {
3904
3697
  const authorization = deps.get("authorization");
3905
3698
  if (authorization === void 0 || typeof authorization.begin !== "function") {
3906
- throw new Error("authorization \u670D\u52A1\u4E0D\u53EF\u7528\uFF1A\u65E0\u6CD5\u8FDB\u884C\u6D4F\u89C8\u5668\u767B\u5F55");
3699
+ throw new Error("authorization \u670D\u52A1\u4E0D\u53EF\u7528\uFF1A\u65E0\u6CD5\u8FDB\u884C GitHub \u767B\u5F55");
3907
3700
  }
3908
3701
  if (!ensureGithubFlow(deps)) {
3909
3702
  throw new Error("authorization \u670D\u52A1\u5C1A\u672A\u5C31\u7EEA\uFF0CGitHub \u6388\u6743\u6D41\u7A0B\u6CA1\u6709\u6CE8\u518C\u6210\u529F\uFF1A\u8BF7\u7A0D\u540E\u91CD\u8BD5");
3910
3703
  }
3911
- if (current !== void 0 && (current.phase === "waiting-browser" || current.phase === "waiting-device" || current.phase === "exchanging")) {
3704
+ if (current !== void 0 && (current.phase === "waiting-device" || current.phase === "exchanging")) {
3912
3705
  throw new Error("already-in-flight");
3913
3706
  }
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
- }
3918
3707
  const controller = new AbortController();
3919
3708
  const timer = setTimeout(() => controller.abort(), attemptTimeout(deps));
3920
3709
  const attempt = { phase: "idle", controller, done: Promise.resolve() };
@@ -3923,15 +3712,13 @@ async function beginGithubSignIn(deps, method = "browser") {
3923
3712
  try {
3924
3713
  started = authorization.begin({
3925
3714
  key: GITHUB_RECORD_KEY2,
3926
- method: effectiveMethod,
3715
+ method: "device",
3927
3716
  signal: controller.signal,
3928
- // 浏览器往返靠 authorizeUrl + 回调;notify 只更新内存视图,prompt 不支持(拒绝)。
3717
+ // 设备码:notify 只更新内存视图;prompt 不支持(拒绝)。
3929
3718
  interaction: {
3930
3719
  notify: (notice) => {
3931
- if (notice.code !== void 0 && notice.url !== void 0) {
3720
+ if (notice.code !== void 0) {
3932
3721
  updateCurrent({ phase: "waiting-device", userCode: notice.code });
3933
- } else if (notice.url !== void 0) {
3934
- updateCurrent({ phase: "waiting-browser", authorizeUrl: notice.url });
3935
3722
  }
3936
3723
  },
3937
3724
  prompt: () => Promise.reject(new Error("interactive prompt unsupported in this surface"))
@@ -3968,7 +3755,6 @@ function resetAttemptForTest() {
3968
3755
  }
3969
3756
  function normalizeEndpoints(endpoints = {}) {
3970
3757
  return {
3971
- authorize: endpoints?.authorize ?? AUTHORIZE_URL,
3972
3758
  accessToken: endpoints?.accessToken ?? ACCESS_TOKEN_URL,
3973
3759
  deviceCode: endpoints?.deviceCode ?? DEVICE_CODE_URL,
3974
3760
  user: endpoints?.user ?? GITHUB_USER_URL
@@ -3977,37 +3763,6 @@ function normalizeEndpoints(endpoints = {}) {
3977
3763
  function attemptTimeout(deps) {
3978
3764
  return typeof deps.attemptTimeoutMs === "number" && deps.attemptTimeoutMs > 0 ? deps.attemptTimeoutMs : DEFAULT_ATTEMPT_TIMEOUT_MS;
3979
3765
  }
3980
- function localOrigin(webServer) {
3981
- const port = webServer.port;
3982
- if (typeof port !== "number" || port <= 0) throw new Error("webServer \u5C1A\u672A\u76D1\u542C\uFF0C\u65E0\u6CD5\u786E\u5B9A\u56DE\u8C03\u5730\u5740");
3983
- const host = typeof webServer.host === "string" && webServer.host !== "" ? webServer.host : "127.0.0.1";
3984
- const loopback = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
3985
- return `http://${loopback}:${port}`;
3986
- }
3987
- function withAbort(promise, signal, onAbort) {
3988
- if (signal === void 0) return promise;
3989
- if (signal.aborted) {
3990
- onAbort();
3991
- return Promise.reject(new Error("authorization cancelled"));
3992
- }
3993
- return new Promise((resolve, reject) => {
3994
- const abort = () => {
3995
- onAbort();
3996
- reject(new Error("authorization cancelled"));
3997
- };
3998
- signal.addEventListener("abort", abort, { once: true });
3999
- promise.then(
4000
- (value) => {
4001
- signal.removeEventListener("abort", abort);
4002
- resolve(value);
4003
- },
4004
- (error) => {
4005
- signal.removeEventListener("abort", abort);
4006
- reject(error);
4007
- }
4008
- );
4009
- });
4010
- }
4011
3766
  function sleep(ms, signal) {
4012
3767
  return new Promise((resolve, reject) => {
4013
3768
  if (signal?.aborted === true) {
@@ -4541,7 +4296,6 @@ function githubAuthDeps(deps) {
4541
4296
  get: deps.get,
4542
4297
  ...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl },
4543
4298
  ...deps.githubClientId === void 0 ? {} : { clientId: deps.githubClientId },
4544
- ...deps.githubClientSecret === void 0 ? {} : { clientSecret: deps.githubClientSecret },
4545
4299
  ...deps.logger === void 0 ? {} : { logger: deps.logger }
4546
4300
  };
4547
4301
  }
@@ -4728,9 +4482,8 @@ async function dispatch(method, pathname, url, body, deps) {
4728
4482
  return { github: probe };
4729
4483
  }
4730
4484
  if (method === "POST" && pathname === "/github/browser/begin") {
4731
- const method2 = body.method === "device" ? "device" : "browser";
4732
4485
  try {
4733
- const view = await beginGithubSignIn(githubAuthDeps(deps), method2);
4486
+ const view = await beginGithubSignIn(githubAuthDeps(deps));
4734
4487
  if (view.phase === "error") throw new IpcError("GITHUB_AUTH_UNAVAILABLE", redact(view.error ?? "GitHub \u767B\u5F55\u5931\u8D25"));
4735
4488
  return { attempt: view };
4736
4489
  } catch (error) {
@@ -4878,15 +4631,10 @@ async function statePayload(deps, settings) {
4878
4631
  authMode: settings.github.authMode,
4879
4632
  configured: github.token !== void 0,
4880
4633
  source: github.source,
4881
- /** 浏览器登录 attempt 视图(idle / waiting-browser / waiting-device / exchanging / done / error)。 */
4634
+ /** GitHub 登录 attempt 视图(idle / waiting-device / exchanging / done / error)。 */
4882
4635
  attempt: attemptView(),
4883
- /** 浏览器登录是否可用:authorization 服务在、且 GitHub flow 真的注册上了(client_id 有内置默认)。 */
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))
4636
+ /** 登录是否可用:authorization 服务在、且 GitHub flow 真的注册上了(client_id 有内置默认)。 */
4637
+ browserAvailable: browserSignInAvailable(deps)
4890
4638
  }
4891
4639
  };
4892
4640
  }
package/lib/index.d.ts CHANGED
@@ -27,7 +27,6 @@ declare function apply(ctx: PluginContext, config?: {
27
27
  stateDir?: string;
28
28
  corePath?: string;
29
29
  githubClientId?: string;
30
- githubClientSecret?: string;
31
30
  }): void;
32
31
  declare const _default: {
33
32
  name: string;
package/lib/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  createUpdateService,
10
10
  ensureGithubFlow,
11
11
  readPluginInfo
12
- } from "./chunk-QOYTABJH.js";
12
+ } from "./chunk-OMQY3T3V.js";
13
13
 
14
14
  // src/index.ts
15
15
  var name = "dsh-github-trending";
@@ -20,7 +20,6 @@ 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();
24
23
  const get = (key) => typeof ctx.get === "function" ? ctx.get(key) : void 0;
25
24
  const log = (event, data) => {
26
25
  const suffix = data === void 0 ? "" : ` ${JSON.stringify(data)}`;
@@ -68,8 +67,7 @@ function apply(ctx, config = {}) {
68
67
  const githubAuth = (resolve) => ({
69
68
  get: resolve,
70
69
  logger: log,
71
- ...config.githubClientId === void 0 ? {} : { clientId: config.githubClientId },
72
- ...githubClientSecret === void 0 || githubClientSecret === "" ? {} : { clientSecret: githubClientSecret }
70
+ ...config.githubClientId === void 0 ? {} : { clientId: config.githubClientId }
73
71
  });
74
72
  const registerAuth = (resolve) => {
75
73
  if (!ensureGithubFlow(githubAuth(resolve ?? get))) log("auth.flow.deferred", { reason: "authorization-not-ready" });
@@ -102,7 +100,6 @@ function apply(ctx, config = {}) {
102
100
  pluginName: pluginInfo.name,
103
101
  ...pluginInfo.repository === void 0 ? {} : { repository: pluginInfo.repository },
104
102
  ...config.githubClientId === void 0 ? {} : { githubClientId: config.githubClientId },
105
- ...githubClientSecret === void 0 || githubClientSecret === "" ? {} : { githubClientSecret },
106
103
  logger: log
107
104
  });
108
105
  const register = () => {
package/lib/internal.d.ts CHANGED
@@ -1157,39 +1157,36 @@ declare namespace github {
1157
1157
  export { type github_GithubProbeResult as GithubProbeResult, github_probeGithubAuth as probeGithubAuth };
1158
1158
  }
1159
1159
 
1160
+ /**
1161
+ * GitHub 浏览器登录(薄 adapter)。
1162
+ *
1163
+ * 设计原则:**只补核心缺的那一层,其余全用核心轮子**(见
1164
+ * `.dsh/docs/plans/2026-11-01-github-browser-auth-plan.md`)。
1165
+ * - 协议生命周期 / attempt 单飞 / 取消 / 结算 → `ctx.authorization`(核心)
1166
+ * - 回环回调 HTTP 载体 → `ctx.webServer.register({ kind:'exact' })`(核心)
1167
+ * - token 落盘 / 原子写 / 0600 / record 事件 → `ctx.credentials`(核心)
1168
+ * - 交互词汇 notify/prompt → `AuthorizationSession`(核心)
1169
+ *
1170
+ * 本文件只写 GitHub provider 专有的:client_id、端点、PKCE 三行、state 校验、exchange、
1171
+ * grant payload,以及「进程内 begin + 内存 attempt 视图」这点胶水(核心没有给第三方 key
1172
+ * 的 authorization HTTP 投影,见阶段 0 核实 R4)。
1173
+ *
1174
+ * 不 import 任何 `@deepseek-ai/*` 运行时模块:全部走 `get(...)` 的鸭子类型,故不需要新增
1175
+ * peerDependency。record 键用字符串 `'github/default'`(与核心包提案同名同构,便于迁移)。
1176
+ */
1160
1177
  /** 与核心 `@deepseek-ai/dsh-github-auth` 提案保持同 key;迁移时消费端无需改。 */
1161
1178
  declare const GITHUB_RECORD_KEY = "github/default";
1162
- /** 本地回调路径(带集成前缀,避免多 provider 撞 webServer 的 `(kind,path)` 唯一约束)。 */
1163
- declare const CALLBACK_PATH = "/api/morehao/github-trending/oauth/github/callback";
1164
1179
  /**
1165
1180
  * 内置的 GitHub OAuth App 公开 client_id(**非机密**:OAuth 的 client_id 设计上就是公开的,
1166
1181
  * 会出现在授权 URL 里,与 `gh`/VS Code 内置自己的 client_id 同理)。
1167
1182
  *
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 已启用)。
1174
- *
1175
- * OAuth App 侧登记的回调:`http://127.0.0.1/api/morehao/github-trending/oauth/github/callback`
1176
- * (loopback 按 RFC 8252 忽略端口,运行时随机端口可正常回跳)。
1183
+ * 只走 **device flow**:GitHub 的 web application flow 换 token 必须带 client_secret,
1184
+ * 而客户端不能内置机密,故本插件不实现回环 web flow(也不支持自带 OAuth App 的 secret)。
1185
+ * device flow 需要在 OAuth App 设置里勾选启用(本插件内置的 App 已启用)。
1177
1186
  */
1178
1187
  declare const DEFAULT_GITHUB_CLIENT_ID = "Ov23li7rhp0EKoqbkJKY";
1179
- /** 解析生效的 client_id:显式 config 优先,否则用内置默认。 */
1188
+ /** 解析生效的 client_id:显式 config 优先,否则用内置默认(device flow 允许自定义 client_id)。 */
1180
1189
  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';
1193
1190
  /** 本插件运行需要 push / 建 PR,`read:user` 不够;一次授权到 `repo`。 */
1194
1191
  declare const DEFAULT_SCOPE = "repo";
1195
1192
  interface AuthorizationFlow {
@@ -1224,15 +1221,6 @@ interface AuthorizationLike {
1224
1221
  /** 核心 0.2.x 有:按 key 查已注册的 flow;判定「真的注册上了」靠它。 */
1225
1222
  describe?: (key: string) => unknown;
1226
1223
  }
1227
- interface WebServerLike {
1228
- register?: (route: {
1229
- kind: 'exact';
1230
- path: string;
1231
- handler: (req: any, res: any) => void;
1232
- }) => () => void;
1233
- port?: number;
1234
- host?: string;
1235
- }
1236
1224
  type FetchLike = typeof fetch;
1237
1225
  interface GithubAuthDeps {
1238
1226
  get: (name: string) => unknown;
@@ -1240,66 +1228,25 @@ interface GithubAuthDeps {
1240
1228
  logger?: (event: string, data?: Record<string, unknown>) => void;
1241
1229
  /** 覆盖端点(测试用);生产留空走 GitHub 官方。 */
1242
1230
  endpoints?: {
1243
- authorize?: string;
1244
1231
  accessToken?: string;
1245
1232
  deviceCode?: string;
1246
1233
  user?: string;
1247
1234
  };
1248
- /** GitHub OAuth App 的 public client_id(无需 secret)。 */
1235
+ /** GitHub OAuth App 的 public client_id(device flow 用;无需 secret)。 */
1249
1236
  clientId?: string;
1250
- /**
1251
- * GitHub OAuth App 的 client_secret。只有自带 OAuth App 的回环 web flow 才需要;
1252
- * 缺省时 `browser` 方法自动降级为 device flow。不进 `settings.json`、不进日志。
1253
- */
1254
- clientSecret?: string;
1255
1237
  scope?: string;
1256
1238
  attemptTimeoutMs?: number;
1257
1239
  }
1258
- type GithubAttemptPhase = 'idle' | 'waiting-browser' | 'waiting-device' | 'exchanging' | 'done' | 'error';
1240
+ type GithubAttemptPhase = 'idle' | 'waiting-device' | 'exchanging' | 'done' | 'error';
1259
1241
  interface GithubAttemptView {
1260
1242
  phase: GithubAttemptPhase;
1261
- authorizeUrl?: string;
1262
1243
  verificationUri?: string;
1263
- /** device flow 预填验证码的地址(GitHub 返回的 `verification_uri_complete`):打开只需点授权。 */
1264
- verificationUriComplete?: string;
1265
1244
  userCode?: string;
1266
1245
  expiresAt?: number;
1267
1246
  login?: string;
1268
1247
  error?: string;
1269
1248
  }
1270
1249
  declare function attemptView(): GithubAttemptView;
1271
- /** 判断一个未知值是否是可直接打开的 http(s) URL(用于校验 GitHub 返回的验证页地址)。 */
1272
- declare function isHttpUrl(value: unknown): boolean;
1273
- /** PKCE:返回 verifier 与 S256 challenge。 */
1274
- declare function createPkce(): {
1275
- verifier: string;
1276
- challenge: string;
1277
- };
1278
- /** 常量时间比较 state(长度不等直接 false,避免 timingSafeEqual 抛错)。 */
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;
1287
- /** 拼授权页 URL。 */
1288
- declare function buildAuthorizeUrl(params: {
1289
- authorizeEndpoint: string;
1290
- clientId: string;
1291
- redirectUri: string;
1292
- scope: string;
1293
- state: string;
1294
- challenge: string;
1295
- }): string;
1296
- interface GithubGrantPayload {
1297
- version: 1;
1298
- token: string;
1299
- login?: string;
1300
- tokenType?: string;
1301
- scope?: string;
1302
- }
1303
1250
  /** 这个 key 现在是否**真的**有 flow 在注册表里。 */
1304
1251
  declare function isGithubFlowRegistered(deps: GithubAuthDeps): boolean;
1305
1252
  /**
@@ -1330,7 +1277,7 @@ declare function registerGithubFlow(deps: GithubAuthDeps): void;
1330
1277
  *
1331
1278
  * @throws 服务不可用、flow 未注册、已有 attempt 在飞时抛错。
1332
1279
  */
1333
- declare function beginGithubSignIn(deps: GithubAuthDeps, method?: 'browser' | 'device'): Promise<GithubAttemptView>;
1280
+ declare function beginGithubSignIn(deps: GithubAuthDeps): Promise<GithubAttemptView>;
1334
1281
  /** 取消进行中的 attempt。 */
1335
1282
  declare function cancelGithubSignIn(deps: GithubAuthDeps): void;
1336
1283
  /** 仅测试用:清空内存 attempt。 */
@@ -1339,33 +1286,22 @@ declare function resetAttemptForTest(): void;
1339
1286
  type githubAuth_AuthorizationFlow = AuthorizationFlow;
1340
1287
  type githubAuth_AuthorizationLike = AuthorizationLike;
1341
1288
  type githubAuth_AuthorizationSessionLike = AuthorizationSessionLike;
1342
- declare const githubAuth_CALLBACK_PATH: typeof CALLBACK_PATH;
1343
1289
  declare const githubAuth_DEFAULT_GITHUB_CLIENT_ID: typeof DEFAULT_GITHUB_CLIENT_ID;
1344
1290
  declare const githubAuth_DEFAULT_SCOPE: typeof DEFAULT_SCOPE;
1345
1291
  declare const githubAuth_GITHUB_RECORD_KEY: typeof GITHUB_RECORD_KEY;
1346
1292
  type githubAuth_GithubAttemptPhase = GithubAttemptPhase;
1347
1293
  type githubAuth_GithubAttemptView = GithubAttemptView;
1348
1294
  type githubAuth_GithubAuthDeps = GithubAuthDeps;
1349
- type githubAuth_GithubGrantPayload = GithubGrantPayload;
1350
- type githubAuth_WebServerLike = WebServerLike;
1351
1295
  declare const githubAuth_attemptView: typeof attemptView;
1352
1296
  declare const githubAuth_beginGithubSignIn: typeof beginGithubSignIn;
1353
- declare const githubAuth_buildAuthorizeUrl: typeof buildAuthorizeUrl;
1354
1297
  declare const githubAuth_cancelGithubSignIn: typeof cancelGithubSignIn;
1355
- declare const githubAuth_createPkce: typeof createPkce;
1356
1298
  declare const githubAuth_ensureGithubFlow: typeof ensureGithubFlow;
1357
1299
  declare const githubAuth_isGithubFlowRegistered: typeof isGithubFlowRegistered;
1358
- declare const githubAuth_isHttpUrl: typeof isHttpUrl;
1359
- declare const githubAuth_isLoopbackSignInAvailable: typeof isLoopbackSignInAvailable;
1360
1300
  declare const githubAuth_registerGithubFlow: typeof registerGithubFlow;
1361
- declare const githubAuth_renderCallbackPage: typeof renderCallbackPage;
1362
1301
  declare const githubAuth_resetAttemptForTest: typeof resetAttemptForTest;
1363
1302
  declare const githubAuth_resolveClientId: typeof resolveClientId;
1364
- declare const githubAuth_resolveClientSecret: typeof resolveClientSecret;
1365
- declare const githubAuth_resolveMethod: typeof resolveMethod;
1366
- declare const githubAuth_verifyState: typeof verifyState;
1367
1303
  declare namespace githubAuth {
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 };
1304
+ export { type githubAuth_AuthorizationFlow as AuthorizationFlow, type githubAuth_AuthorizationLike as AuthorizationLike, type githubAuth_AuthorizationSessionLike as AuthorizationSessionLike, 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, githubAuth_attemptView as attemptView, githubAuth_beginGithubSignIn as beginGithubSignIn, githubAuth_cancelGithubSignIn as cancelGithubSignIn, githubAuth_ensureGithubFlow as ensureGithubFlow, githubAuth_isGithubFlowRegistered as isGithubFlowRegistered, githubAuth_registerGithubFlow as registerGithubFlow, githubAuth_resetAttemptForTest as resetAttemptForTest, githubAuth_resolveClientId as resolveClientId };
1369
1305
  }
1370
1306
 
1371
1307
  interface DefaultBranchResult {
@@ -2049,10 +1985,8 @@ interface RouteDeps {
2049
1985
  /** 包名与仓库地址都来自 package.json,由 src/index.ts 传入。 */
2050
1986
  pluginName?: string;
2051
1987
  repository?: string;
2052
- /** 浏览器 GitHub 登录用的 public client_id(由 apply 的 config 传入;缺省时用内置默认)。 */
1988
+ /** GitHub 登录用的 public client_id(由 apply 的 config 传入;缺省时用内置默认)。 */
2053
1989
  githubClientId?: string;
2054
- /** 自带 OAuth App 时的 client_secret(只用于回环 web flow;缺省时该 method 自动降级为设备码)。 */
2055
- githubClientSecret?: string;
2056
1990
  logger?: (event: string, data?: Record<string, unknown>) => void;
2057
1991
  }
2058
1992
  declare function createRoutes(deps: RouteDeps): (request: http.IncomingMessage, response: http.ServerResponse) => Promise<void>;
package/lib/internal.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  tools_exports,
28
28
  update_exports,
29
29
  version_exports
30
- } from "./chunk-QOYTABJH.js";
30
+ } from "./chunk-OMQY3T3V.js";
31
31
 
32
32
  // src/internal.ts
33
33
  import * as exec from "@morehao/dsh-plugin-kit/exec";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morehao/dsh-github-trending",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "GitHub 趋势日报的 DeepSeek Harness 插件:按日 / 周 / 月周期定时 spawn 本地 trendradar 核心,抓取 Trending、生成中文简介与周期总结,并可选把订阅仓库的新 Release 合成中文摘要,一起提交到你的仓库、自动开 PR 合并;内置概览 / 榜单 / 运行记录 / 订阅 / 设置工作台。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",