@morehao/dsh-github-trending 0.8.5 → 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,17 @@
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
+
3
15
  ## [v0.8.5] - 2026-11-02
4
16
 
5
17
  ### Fixes
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
@@ -127,19 +127,22 @@ var DICT = {
127
127
  "auth.gh.hint": "使用本机 gh 登录态:终端执行 `gh auth login` 完成登录,再点「测试认证」。",
128
128
  "auth.browser.hint": "在浏览器里完成 GitHub 授权,token 由 DSH 安全保存,无需手工粘贴。点一下会打开 GitHub 验证页,把下方验证码输入并确认即可。",
129
129
  "auth.browser.signin": "用 GitHub 登录",
130
- "auth.browser.waiting": "等待浏览器授权…",
131
- "auth.browser.code": "验证码:",
132
130
  "auth.browser.copyCode": "复制验证码",
133
131
  "auth.browser.codeCopied": "验证码已复制",
134
132
  "auth.browser.codeCopyFailed": "复制失败,请手动记下验证码",
135
133
  "auth.browser.cancel": "取消",
136
134
  "auth.browser.done": "已登录",
137
- "auth.browser.device": "用设备码登录",
138
135
  "auth.browser.unavailable": "宿主的 authorization 服务不可用(或 GitHub 授权流程没注册成功):请重试一次;仍不行就改用「本机 gh」或 PAT。",
139
- "auth.browser.openHint": "若浏览器没有自动打开,点下面的链接完成授权,或复制链接:",
140
- "auth.browser.open": "打开授权页",
141
- "auth.browser.copy": "复制链接",
142
- "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": "查看验证码",
143
146
  "auth.browser.failed": "登录失败",
144
147
  "auth.browser.failed.detail": "登录失败:{message}",
145
148
  "auth.browser.failed.hint": "请重试一次;仍失败可改用「本机 gh」或 PAT。",
@@ -675,19 +678,22 @@ var DICT = {
675
678
  "auth.gh.hint": "Uses your local gh login: run `gh auth login` in a terminal, then click Test authentication.",
676
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.",
677
680
  "auth.browser.signin": "Sign in with GitHub",
678
- "auth.browser.waiting": "Waiting for browser authorization…",
679
- "auth.browser.code": "Verification code:",
680
681
  "auth.browser.copyCode": "Copy code",
681
682
  "auth.browser.codeCopied": "Code copied",
682
683
  "auth.browser.codeCopyFailed": "Copy failed — please note the code manually",
683
684
  "auth.browser.cancel": "Cancel",
684
685
  "auth.browser.done": "Signed in",
685
- "auth.browser.device": "Use a device code",
686
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.",
687
- "auth.browser.openHint": "If the browser did not open, use the link below to finish signing in, or copy the link:",
688
- "auth.browser.open": "Open authorization page",
689
- "auth.browser.copy": "Copy link",
690
- "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",
691
697
  "auth.browser.failed": "Sign-in failed",
692
698
  "auth.browser.failed.detail": "Sign-in failed: {message}",
693
699
  "auth.browser.failed.hint": "Retry once; if it keeps failing, use Local gh or a PAT instead.",
@@ -1238,6 +1244,20 @@ var CSS = `
1238
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;}
1239
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);}
1240
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
+
1241
1261
  /* ——— 01 工作台外壳 ——— */
1242
1262
  .dshgt-shell{gap:0;height:100%;min-height:0;overflow:hidden;}
1243
1263
  .dshgt-app{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--gt-bg);}
@@ -1891,8 +1911,8 @@ async function apiCall(method, path, body, options = {}) {
1891
1911
  }
1892
1912
  return json;
1893
1913
  }
1894
- async function beginGithubSignIn(method = "browser") {
1895
- const payload = await apiCall("POST", "/github/browser/begin", { method });
1914
+ async function beginGithubSignIn() {
1915
+ const payload = await apiCall("POST", "/github/browser/begin", {});
1896
1916
  return payload.attempt;
1897
1917
  }
1898
1918
  async function getGithubSignInStatus(signal) {
@@ -2136,6 +2156,7 @@ function SettingsView(props) {
2136
2156
  const [testing, setTesting] = import_react.default.useState(false);
2137
2157
  const [githubTest, setGithubTest] = import_react.default.useState(null);
2138
2158
  const [signIn, setSignIn] = import_react.default.useState({ phase: "idle" });
2159
+ const [signInModal, setSignInModal] = import_react.default.useState(false);
2139
2160
  const anyDirty = SECTIONS.some((id) => dirtySections[id]);
2140
2161
  const sectionEditable = section !== "source";
2141
2162
  import_react.default.useEffect(() => {
@@ -2263,50 +2284,43 @@ function SettingsView(props) {
2263
2284
  setTesting(false);
2264
2285
  }
2265
2286
  };
2266
- 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" });
2267
2297
  try {
2268
- let view = await beginGithubSignIn(method);
2298
+ let view = await beginGithubSignIn();
2269
2299
  if (view.phase === "idle") view = await getGithubSignInStatus();
2270
2300
  setSignIn(view);
2271
- if (view.phase === "error") {
2272
- notify("err", t("auth.browser.failed.detail", { message: view.error ?? "" }));
2273
- return;
2274
- }
2275
- if (view.phase === "waiting-browser" && view.authorizeUrl !== void 0) {
2276
- try {
2277
- window.open(view.authorizeUrl, "_blank", "noopener,noreferrer");
2278
- } catch {
2279
- }
2280
- }
2281
- const deviceUrl = view.verificationUriComplete ?? view.verificationUri;
2282
- if (view.phase === "waiting-device" && deviceUrl !== void 0) {
2283
- try {
2284
- window.open(deviceUrl, "_blank", "noopener,noreferrer");
2285
- } catch {
2286
- }
2287
- }
2301
+ if (view.phase === "waiting-device") openVerifyPage(view.verificationUri);
2288
2302
  } catch (error) {
2289
- notify("err", error instanceof Error ? error.message : String(error));
2303
+ setSignIn({ phase: "error", error: error instanceof Error ? error.message : String(error) });
2290
2304
  }
2291
2305
  };
2306
+ const openSignInModal = () => setSignInModal(true);
2307
+ const closeSignInModal = () => setSignInModal(false);
2292
2308
  const cancelSignIn = async () => {
2293
2309
  try {
2294
2310
  setSignIn(await cancelGithubSignIn());
2295
2311
  } catch (error) {
2296
2312
  notify("err", error instanceof Error ? error.message : String(error));
2313
+ } finally {
2314
+ setSignInModal(false);
2297
2315
  }
2298
2316
  };
2299
- const copyAuthorizeLink = () => {
2300
- if (signIn.authorizeUrl === void 0) return;
2301
- void navigator.clipboard?.writeText(signIn.authorizeUrl).then(() => notify("ok", t("auth.browser.copied")));
2302
- };
2303
2317
  const copyDeviceCode = () => {
2304
2318
  if (signIn.userCode === void 0) return;
2305
2319
  void copyText(signIn.userCode).then(
2306
2320
  (ok) => notify(ok ? "ok" : "err", ok ? t("auth.browser.codeCopied") : t("auth.browser.codeCopyFailed"))
2307
2321
  );
2308
2322
  };
2309
- const signInActive = signIn.phase === "waiting-browser" || signIn.phase === "waiting-device" || signIn.phase === "exchanging";
2323
+ const signInActive = signIn.phase === "waiting-device" || signIn.phase === "exchanging";
2310
2324
  import_react.default.useEffect(() => {
2311
2325
  if (!signInActive) return void 0;
2312
2326
  let alive = true;
@@ -2367,7 +2381,6 @@ function SettingsView(props) {
2367
2381
  const isGh = draft.github.authMode === "gh";
2368
2382
  const isBrowser = draft.github.authMode === "browser";
2369
2383
  const browserAvailable = state.github.browserAvailable === true;
2370
- const loopbackAvailable = state.github.loopbackAvailable === true;
2371
2384
  const sections = [
2372
2385
  { id: "source", label: t("st.source") },
2373
2386
  { id: "model", label: t("st.model") },
@@ -2562,13 +2575,10 @@ function SettingsView(props) {
2562
2575
  {
2563
2576
  small: true,
2564
2577
  variant: "primary",
2565
- onClick: () => void startSignIn(loopbackAvailable ? "browser" : "device"),
2566
- disabled: signInActive
2578
+ onClick: () => void (signInActive ? openSignInModal() : startSignIn())
2567
2579
  },
2568
- signIn.phase === "done" && signIn.login !== void 0 ? `${t("auth.browser.done")} · @${signIn.login}` : signInActive ? t("auth.browser.waiting") : t("auth.browser.signin")
2569
- ) : h("span", { className: "dshgt-hint" }, t("auth.browser.unavailable")),
2570
- browserAvailable && loopbackAvailable ? h(Btn, { small: true, variant: "ghost", onClick: () => void startSignIn("device"), disabled: signInActive }, t("auth.browser.device")) : null,
2571
- 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"))
2572
2582
  ) : h(
2573
2583
  Row,
2574
2584
  { label: t("auth.tokenRef") },
@@ -2577,47 +2587,6 @@ function SettingsView(props) {
2577
2587
  }) }),
2578
2588
  h("span", { className: "dshgt-hint" }, t("auth.tokenRef.hint"))
2579
2589
  ),
2580
- // 浏览器登录失败:必须显式说出来,否则「点了没反应」又回来了。
2581
- isBrowser && signIn.phase === "error" ? h(Alert, { kind: "err", title: t("auth.browser.failed") }, signIn.error ?? t("auth.browser.failed.hint")) : null,
2582
- // 浏览器登录进行中的状态行:可点的授权页链接 + 复制兜底 + 设备码。
2583
- isBrowser && (signIn.phase === "waiting-browser" || signIn.phase === "waiting-device" || signIn.phase === "exchanging") ? h(
2584
- "div",
2585
- { className: "dshgt-hint" },
2586
- signIn.authorizeUrl !== void 0 ? h(
2587
- "div",
2588
- { className: "dshgt-desc" },
2589
- h("span", null, t("auth.browser.openHint")),
2590
- " ",
2591
- h(
2592
- "a",
2593
- { className: "dshgt-link", href: signIn.authorizeUrl, target: "_blank", rel: "noreferrer" },
2594
- t("auth.browser.open")
2595
- ),
2596
- " ",
2597
- h(Btn, { small: true, variant: "ghost", onClick: () => copyAuthorizeLink() }, t("auth.browser.copy"))
2598
- ) : null,
2599
- signIn.verificationUriComplete !== void 0 || signIn.verificationUri !== void 0 ? h(
2600
- "div",
2601
- { className: "dshgt-desc" },
2602
- h(
2603
- "a",
2604
- {
2605
- className: "dshgt-link",
2606
- href: signIn.verificationUriComplete ?? signIn.verificationUri,
2607
- target: "_blank",
2608
- rel: "noreferrer"
2609
- },
2610
- t("auth.browser.open")
2611
- )
2612
- ) : null,
2613
- signIn.userCode !== void 0 ? h(
2614
- "div",
2615
- { className: "dshgt-desc", style: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" } },
2616
- h("span", null, t("auth.browser.code")),
2617
- h("span", { className: "dshgt-mono", style: { fontSize: "1.1em", fontWeight: "bold", letterSpacing: "1px" } }, signIn.userCode),
2618
- h(Btn, { small: true, variant: "ghost", onClick: () => copyDeviceCode() }, t("auth.browser.copyCode"))
2619
- ) : null
2620
- ) : null,
2621
2590
  h(
2622
2591
  Row,
2623
2592
  { label: t("auth.test") },
@@ -2891,7 +2860,88 @@ function SettingsView(props) {
2891
2860
  setDirtySections({ ...EMPTY_DIRTY });
2892
2861
  setSavedSection(null);
2893
2862
  void store.refresh({ quiet: true });
2894
- } }) : 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
+ )
2895
2945
  );
2896
2946
  }
2897
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,70 +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
- /**
1264
- * device flow 的预填验证码地址(`verification_uri_complete`),**可选**:若 provider 返回就透出,
1265
- * 前端优先用它打开(码已预填)。注意 GitHub 官方 device flow 实测不返回该字段,此时走
1266
- * `verificationUri` + 手动输入;保留此字段只为兼容会返回它的 provider。
1267
- */
1268
- verificationUriComplete?: string;
1269
1244
  userCode?: string;
1270
1245
  expiresAt?: number;
1271
1246
  login?: string;
1272
1247
  error?: string;
1273
1248
  }
1274
1249
  declare function attemptView(): GithubAttemptView;
1275
- /** 判断一个未知值是否是可直接打开的 http(s) URL(用于校验 GitHub 返回的验证页地址)。 */
1276
- declare function isHttpUrl(value: unknown): boolean;
1277
- /** PKCE:返回 verifier 与 S256 challenge。 */
1278
- declare function createPkce(): {
1279
- verifier: string;
1280
- challenge: string;
1281
- };
1282
- /** 常量时间比较 state(长度不等直接 false,避免 timingSafeEqual 抛错)。 */
1283
- declare function verifyState(received: string, expected: string): boolean;
1284
- /** 回调落地页的两种结果(纯展示,无外部资源,离线可测)。 */
1285
- type CallbackPageKind = 'success' | 'error';
1286
- /**
1287
- * 回环回调的落地页 HTML。自包含、无外链资源(CSP 友好、离线可用),
1288
- * 跟随系统深浅色;成功页会尝试自动关闭本窗口(脚本失败时文字兜底)。
1289
- */
1290
- declare function renderCallbackPage(kind: CallbackPageKind): string;
1291
- /** 拼授权页 URL。 */
1292
- declare function buildAuthorizeUrl(params: {
1293
- authorizeEndpoint: string;
1294
- clientId: string;
1295
- redirectUri: string;
1296
- scope: string;
1297
- state: string;
1298
- challenge: string;
1299
- }): string;
1300
- interface GithubGrantPayload {
1301
- version: 1;
1302
- token: string;
1303
- login?: string;
1304
- tokenType?: string;
1305
- scope?: string;
1306
- }
1307
1250
  /** 这个 key 现在是否**真的**有 flow 在注册表里。 */
1308
1251
  declare function isGithubFlowRegistered(deps: GithubAuthDeps): boolean;
1309
1252
  /**
@@ -1334,7 +1277,7 @@ declare function registerGithubFlow(deps: GithubAuthDeps): void;
1334
1277
  *
1335
1278
  * @throws 服务不可用、flow 未注册、已有 attempt 在飞时抛错。
1336
1279
  */
1337
- declare function beginGithubSignIn(deps: GithubAuthDeps, method?: 'browser' | 'device'): Promise<GithubAttemptView>;
1280
+ declare function beginGithubSignIn(deps: GithubAuthDeps): Promise<GithubAttemptView>;
1338
1281
  /** 取消进行中的 attempt。 */
1339
1282
  declare function cancelGithubSignIn(deps: GithubAuthDeps): void;
1340
1283
  /** 仅测试用:清空内存 attempt。 */
@@ -1343,33 +1286,22 @@ declare function resetAttemptForTest(): void;
1343
1286
  type githubAuth_AuthorizationFlow = AuthorizationFlow;
1344
1287
  type githubAuth_AuthorizationLike = AuthorizationLike;
1345
1288
  type githubAuth_AuthorizationSessionLike = AuthorizationSessionLike;
1346
- declare const githubAuth_CALLBACK_PATH: typeof CALLBACK_PATH;
1347
1289
  declare const githubAuth_DEFAULT_GITHUB_CLIENT_ID: typeof DEFAULT_GITHUB_CLIENT_ID;
1348
1290
  declare const githubAuth_DEFAULT_SCOPE: typeof DEFAULT_SCOPE;
1349
1291
  declare const githubAuth_GITHUB_RECORD_KEY: typeof GITHUB_RECORD_KEY;
1350
1292
  type githubAuth_GithubAttemptPhase = GithubAttemptPhase;
1351
1293
  type githubAuth_GithubAttemptView = GithubAttemptView;
1352
1294
  type githubAuth_GithubAuthDeps = GithubAuthDeps;
1353
- type githubAuth_GithubGrantPayload = GithubGrantPayload;
1354
- type githubAuth_WebServerLike = WebServerLike;
1355
1295
  declare const githubAuth_attemptView: typeof attemptView;
1356
1296
  declare const githubAuth_beginGithubSignIn: typeof beginGithubSignIn;
1357
- declare const githubAuth_buildAuthorizeUrl: typeof buildAuthorizeUrl;
1358
1297
  declare const githubAuth_cancelGithubSignIn: typeof cancelGithubSignIn;
1359
- declare const githubAuth_createPkce: typeof createPkce;
1360
1298
  declare const githubAuth_ensureGithubFlow: typeof ensureGithubFlow;
1361
1299
  declare const githubAuth_isGithubFlowRegistered: typeof isGithubFlowRegistered;
1362
- declare const githubAuth_isHttpUrl: typeof isHttpUrl;
1363
- declare const githubAuth_isLoopbackSignInAvailable: typeof isLoopbackSignInAvailable;
1364
1300
  declare const githubAuth_registerGithubFlow: typeof registerGithubFlow;
1365
- declare const githubAuth_renderCallbackPage: typeof renderCallbackPage;
1366
1301
  declare const githubAuth_resetAttemptForTest: typeof resetAttemptForTest;
1367
1302
  declare const githubAuth_resolveClientId: typeof resolveClientId;
1368
- declare const githubAuth_resolveClientSecret: typeof resolveClientSecret;
1369
- declare const githubAuth_resolveMethod: typeof resolveMethod;
1370
- declare const githubAuth_verifyState: typeof verifyState;
1371
1303
  declare namespace githubAuth {
1372
- 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 };
1373
1305
  }
1374
1306
 
1375
1307
  interface DefaultBranchResult {
@@ -2053,10 +1985,8 @@ interface RouteDeps {
2053
1985
  /** 包名与仓库地址都来自 package.json,由 src/index.ts 传入。 */
2054
1986
  pluginName?: string;
2055
1987
  repository?: string;
2056
- /** 浏览器 GitHub 登录用的 public client_id(由 apply 的 config 传入;缺省时用内置默认)。 */
1988
+ /** GitHub 登录用的 public client_id(由 apply 的 config 传入;缺省时用内置默认)。 */
2057
1989
  githubClientId?: string;
2058
- /** 自带 OAuth App 时的 client_secret(只用于回环 web flow;缺省时该 method 自动降级为设备码)。 */
2059
- githubClientSecret?: string;
2060
1990
  logger?: (event: string, data?: Record<string, unknown>) => void;
2061
1991
  }
2062
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.5",
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",