@morehao/dsh-github-trending 0.7.11 → 0.8.1

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/README.md CHANGED
@@ -55,6 +55,16 @@ GitHub 趋势榜的 **DeepSeek Harness 插件**:按日 / 周 / 月周期定时
55
55
 
56
56
  ![设置](docs/images/settings.png)
57
57
 
58
+ ### GitHub 认证
59
+
60
+ 认证分区支持三种方式(可随时切换):
61
+
62
+ - **浏览器登录(默认推荐)**:点「用 GitHub 登录」在浏览器完成授权,无需手工粘贴 token。走 OAuth PKCE + 本机回环回调(或设备码),token 由 DSH 凭据记录安全保存,不落 `settings.json`。插件内置了公开的 OAuth App client_id(client_id 属公开标识、非机密;纯 PKCE 不使用 client_secret),开箱即用。
63
+ - **本机 gh**:复用你终端里 `gh auth login` 的登录态。
64
+ - **PAT**:手工填 Personal Access Token(存到凭据引用 / 环境变量)。
65
+
66
+ > 浏览器登录需要核心提供 `authorization` 服务;若当前核心版本不含该能力,「浏览器登录」会提示不可用并引导改用 gh / PAT。
67
+
58
68
  ### 订阅
59
69
 
60
70
  (可选)订阅一组仓库,每周把每个仓库当周新增的 Release 合成一段中文摘要,与榜单同分支、同 PR 一起提交;订阅列表随数据仓版本化,可审计、可复现。
package/client.js CHANGED
@@ -123,7 +123,19 @@ var DICT = {
123
123
  "auth.mode": "方式",
124
124
  "auth.pat": "PAT",
125
125
  "auth.gh": "本机 gh",
126
+ "auth.browser": "浏览器登录",
126
127
  "auth.gh.hint": "使用本机 gh 登录态:终端执行 `gh auth login` 完成登录,再点「测试认证」。",
128
+ "auth.browser.hint": "在浏览器里完成 GitHub 授权,token 由 DSH 安全保存,无需手工粘贴。",
129
+ "auth.browser.signin": "用 GitHub 登录",
130
+ "auth.browser.waiting": "等待浏览器授权…",
131
+ "auth.browser.code": "在打开的页面输入验证码:",
132
+ "auth.browser.cancel": "取消",
133
+ "auth.browser.done": "已登录",
134
+ "auth.browser.device": "用设备码登录",
135
+ "auth.browser.unavailable": "当前环境未配置 GitHub OAuth client_id,浏览器登录不可用:请改用「本机 gh」或 PAT。",
136
+ "auth.browser.openHint": "若浏览器未自动打开,请手动打开发起授权,或复制链接:",
137
+ "auth.browser.copy": "复制链接",
138
+ "auth.browser.copied": "授权链接已复制",
127
139
  "auth.pat.hint": "在 GitHub → Settings → Developer settings 生成 Personal Access Token(勾选 public_repo),填到下方引用或环境变量。",
128
140
  "auth.tokenRef": "凭据引用",
129
141
  "auth.tokenRef.hint": "凭据引用名 / 环境变量名",
@@ -652,7 +664,19 @@ var DICT = {
652
664
  "auth.mode": "Mode",
653
665
  "auth.pat": "PAT",
654
666
  "auth.gh": "Local gh",
667
+ "auth.browser": "Browser login",
655
668
  "auth.gh.hint": "Uses your local gh login: run `gh auth login` in a terminal, then click Test authentication.",
669
+ "auth.browser.hint": "Authorize GitHub in your browser; DSH stores the token securely, no manual paste.",
670
+ "auth.browser.signin": "Sign in with GitHub",
671
+ "auth.browser.waiting": "Waiting for browser authorization…",
672
+ "auth.browser.code": "Enter this code on the opened page:",
673
+ "auth.browser.cancel": "Cancel",
674
+ "auth.browser.done": "Signed in",
675
+ "auth.browser.device": "Use a device code",
676
+ "auth.browser.unavailable": "No GitHub OAuth client_id configured in this environment, so browser sign-in is unavailable: use Local gh or PAT instead.",
677
+ "auth.browser.openHint": "If the browser did not open, open the authorization page manually, or copy the link:",
678
+ "auth.browser.copy": "Copy link",
679
+ "auth.browser.copied": "Authorization link copied",
656
680
  "auth.pat.hint": "Create a Personal Access Token under GitHub → Settings → Developer settings (scope public_repo) and put it in the ref below or an env var.",
657
681
  "auth.tokenRef": "Credential ref",
658
682
  "auth.tokenRef.hint": "credential ref / env var name",
@@ -1851,6 +1875,23 @@ async function apiCall(method, path, body, options = {}) {
1851
1875
  }
1852
1876
  return json;
1853
1877
  }
1878
+ async function beginGithubSignIn(method = "browser") {
1879
+ const payload = await apiCall("POST", "/github/browser/begin", { method });
1880
+ return payload.attempt;
1881
+ }
1882
+ async function getGithubSignInStatus(signal) {
1883
+ const payload = await apiCall(
1884
+ "GET",
1885
+ "/github/browser/status",
1886
+ void 0,
1887
+ signal === void 0 ? {} : { signal }
1888
+ );
1889
+ return payload.attempt;
1890
+ }
1891
+ async function cancelGithubSignIn() {
1892
+ const payload = await apiCall("POST", "/github/browser/cancel", {});
1893
+ return payload.attempt;
1894
+ }
1854
1895
  function releaseRunsOf(state) {
1855
1896
  if (state === null || state === void 0) return [];
1856
1897
  const root = Array.isArray(state.releaseRuns) ? state.releaseRuns : void 0;
@@ -2078,6 +2119,7 @@ function SettingsView(props) {
2078
2119
  const [branchLoading, setBranchLoading] = import_react.default.useState(false);
2079
2120
  const [testing, setTesting] = import_react.default.useState(false);
2080
2121
  const [githubTest, setGithubTest] = import_react.default.useState(null);
2122
+ const [signIn, setSignIn] = import_react.default.useState({ phase: "idle" });
2081
2123
  const anyDirty = SECTIONS.some((id) => dirtySections[id]);
2082
2124
  const sectionEditable = section !== "source";
2083
2125
  import_react.default.useEffect(() => {
@@ -2205,6 +2247,52 @@ function SettingsView(props) {
2205
2247
  setTesting(false);
2206
2248
  }
2207
2249
  };
2250
+ const startSignIn = async (method) => {
2251
+ try {
2252
+ const view = await beginGithubSignIn(method);
2253
+ setSignIn(view);
2254
+ if (view.phase === "waiting-browser" && view.authorizeUrl !== void 0) {
2255
+ try {
2256
+ window.open(view.authorizeUrl, "_blank", "noopener,noreferrer");
2257
+ } catch {
2258
+ }
2259
+ }
2260
+ } catch (error) {
2261
+ notify("err", error instanceof Error ? error.message : String(error));
2262
+ }
2263
+ };
2264
+ const cancelSignIn = async () => {
2265
+ try {
2266
+ setSignIn(await cancelGithubSignIn());
2267
+ } catch (error) {
2268
+ notify("err", error instanceof Error ? error.message : String(error));
2269
+ }
2270
+ };
2271
+ const copyAuthorizeLink = () => {
2272
+ if (signIn.authorizeUrl === void 0) return;
2273
+ void navigator.clipboard?.writeText(signIn.authorizeUrl).then(() => notify("ok", t("auth.browser.copied")));
2274
+ };
2275
+ const signInActive = signIn.phase === "waiting-browser" || signIn.phase === "waiting-device" || signIn.phase === "exchanging";
2276
+ import_react.default.useEffect(() => {
2277
+ if (!signInActive) return void 0;
2278
+ let alive = true;
2279
+ const timer = setInterval(() => {
2280
+ void getGithubSignInStatus().then((view) => {
2281
+ if (!alive) return;
2282
+ setSignIn(view);
2283
+ if (view.phase === "done") {
2284
+ setGithubTest(view.login === void 0 ? { ok: true } : { ok: true, login: view.login });
2285
+ notify("ok", t("auth.browser.done"));
2286
+ void store.refresh({ quiet: true });
2287
+ }
2288
+ }).catch(() => {
2289
+ });
2290
+ }, 1500);
2291
+ return () => {
2292
+ alive = false;
2293
+ clearInterval(timer);
2294
+ };
2295
+ }, [signInActive]);
2208
2296
  const importCli = async () => {
2209
2297
  try {
2210
2298
  const result = await apiCall(
@@ -2243,6 +2331,8 @@ function SettingsView(props) {
2243
2331
  const branchText = branchLoading ? t("branch.loading") : branchInfo?.branch !== void 0 ? branchInfo.branch : draft.repo.baseBranch !== "" ? draft.repo.baseBranch : "—";
2244
2332
  const branchHint = branchInfo?.source === "origin-head" ? t("branch.fromOrigin") : branchInfo?.source === "head" ? t("branch.fromHead") : branchInfo?.message !== void 0 ? branchInfo.message : t("branch.readonlyHint");
2245
2333
  const isGh = draft.github.authMode === "gh";
2334
+ const isBrowser = draft.github.authMode === "browser";
2335
+ const browserAvailable = state.github.browserAvailable === true;
2246
2336
  const sections = [
2247
2337
  { id: "source", label: t("st.source") },
2248
2338
  { id: "model", label: t("st.model") },
@@ -2419,16 +2509,32 @@ function SettingsView(props) {
2419
2509
  value: draft.github.authMode,
2420
2510
  options: [
2421
2511
  { value: "gh", label: t("auth.gh") },
2422
- { value: "pat", label: t("auth.pat") }
2512
+ { value: "pat", label: t("auth.pat") },
2513
+ { value: "browser", label: t("auth.browser") }
2423
2514
  ],
2424
2515
  onChange: (value) => update((next) => {
2425
- next.github.authMode = value === "pat" ? "pat" : "gh";
2516
+ next.github.authMode = value === "pat" ? "pat" : value === "browser" ? "browser" : "gh";
2426
2517
  })
2427
2518
  }),
2428
2519
  h(Pill, { kind: state.github.configured ? "ok" : "warn" }, state.github.configured ? `${t("auth.configured")} · ${state.github.source}` : t("auth.missing"))
2429
2520
  ),
2430
- h("p", { className: "dshgt-hint" }, isGh ? t("auth.gh.hint") : t("auth.pat.hint")),
2431
- isGh ? null : h(
2521
+ h("p", { className: "dshgt-hint" }, isGh ? t("auth.gh.hint") : isBrowser ? t("auth.browser.hint") : t("auth.pat.hint")),
2522
+ isGh ? null : isBrowser ? h(
2523
+ Row,
2524
+ { label: t("auth.status") },
2525
+ browserAvailable ? h(
2526
+ Btn,
2527
+ {
2528
+ small: true,
2529
+ variant: "primary",
2530
+ onClick: () => void startSignIn("browser"),
2531
+ disabled: signInActive
2532
+ },
2533
+ signIn.phase === "done" && signIn.login !== void 0 ? `${t("auth.browser.done")} · @${signIn.login}` : signInActive ? t("auth.browser.waiting") : t("auth.browser.signin")
2534
+ ) : h("span", { className: "dshgt-hint" }, t("auth.browser.unavailable")),
2535
+ browserAvailable ? h(Btn, { small: true, variant: "ghost", onClick: () => void startSignIn("device"), disabled: signInActive }, t("auth.browser.device")) : null,
2536
+ signInActive ? h(Btn, { small: true, variant: "ghost", onClick: () => void cancelSignIn() }, t("auth.browser.cancel")) : null
2537
+ ) : h(
2432
2538
  Row,
2433
2539
  { label: t("auth.tokenRef") },
2434
2540
  h(Input, { mono: true, width: 220, value: draft.github.tokenRef, placeholder: "GITHUB_TOKEN", onInput: (value) => update((next) => {
@@ -2436,6 +2542,19 @@ function SettingsView(props) {
2436
2542
  }) }),
2437
2543
  h("span", { className: "dshgt-hint" }, t("auth.tokenRef.hint"))
2438
2544
  ),
2545
+ // 浏览器登录进行中的状态行:授权页链接(可复制)+ 设备码。
2546
+ isBrowser && signIn.phase !== "idle" && signIn.phase !== "done" ? h(
2547
+ "div",
2548
+ { className: "dshgt-hint" },
2549
+ signIn.authorizeUrl !== void 0 ? h(
2550
+ "div",
2551
+ { className: "dshgt-desc" },
2552
+ h("span", null, t("auth.browser.openHint")),
2553
+ " ",
2554
+ h(Btn, { small: true, variant: "ghost", onClick: () => copyAuthorizeLink() }, t("auth.browser.copy"))
2555
+ ) : null,
2556
+ signIn.userCode !== void 0 ? h("div", { className: "dshgt-mono" }, `${t("auth.browser.code")} ${signIn.userCode}`) : null
2557
+ ) : null,
2439
2558
  h(
2440
2559
  Row,
2441
2560
  { label: t("auth.test") },
@@ -205,8 +205,8 @@ function normalizeSettings(input) {
205
205
  labels: strArray(pr.labels, DEFAULT_SETTINGS.pr.labels)
206
206
  },
207
207
  github: {
208
- // 旧版本可能存 'device':它没有对应实现,统一迁移到真正可用的「本机 gh」。
209
- authMode: authMode === "pat" ? "pat" : "gh",
208
+ // 白名单三值:'pat' / 'gh' / 'browser';其余(含旧版 'device')统一迁移到真正可用的「本机 gh」。
209
+ authMode: authMode === "pat" || authMode === "browser" ? authMode : "gh",
210
210
  tokenRef: str(github.tokenRef, DEFAULT_SETTINGS.github.tokenRef).trim() || DEFAULT_SETTINGS.github.tokenRef
211
211
  },
212
212
  trending: {
@@ -784,6 +784,11 @@ var PLUGIN_CODES = [
784
784
  "MODEL_NOT_FOUND",
785
785
  "MODEL_PROVIDER_UNSUPPORTED",
786
786
  "GITHUB_AUTH",
787
+ "GITHUB_AUTH_CANCELLED",
788
+ "GITHUB_AUTH_EXPIRED",
789
+ "GITHUB_AUTH_STATE",
790
+ "GITHUB_AUTH_EXCHANGE",
791
+ "GITHUB_AUTH_UNAVAILABLE",
787
792
  "PUSH_FAILED",
788
793
  "REPO_DIRTY",
789
794
  "SUBSCRIBE_FAILED"
@@ -819,6 +824,11 @@ var RETRIABLE = {
819
824
  MODEL_NOT_FOUND: false,
820
825
  MODEL_PROVIDER_UNSUPPORTED: false,
821
826
  GITHUB_AUTH: false,
827
+ GITHUB_AUTH_CANCELLED: false,
828
+ GITHUB_AUTH_EXPIRED: true,
829
+ GITHUB_AUTH_STATE: false,
830
+ GITHUB_AUTH_EXCHANGE: true,
831
+ GITHUB_AUTH_UNAVAILABLE: false,
822
832
  PUSH_FAILED: true,
823
833
  SUBSCRIBE_FAILED: true
824
834
  };
@@ -829,7 +839,7 @@ function areaOf(code) {
829
839
  if (code.startsWith("CORE_") || code === "IPC_INVALID") return "core";
830
840
  if (code.startsWith("MODEL_")) return "model";
831
841
  if (code.startsWith("REPO_") || code === "GIT_NOT_INSTALLED" || code === "GIT_OPERATION_FAILED") return "repo";
832
- if (code === "GITHUB_AUTH") return "auth";
842
+ if (code === "GITHUB_AUTH" || code.startsWith("GITHUB_AUTH_")) return "auth";
833
843
  return "run";
834
844
  }
835
845
  function reasonOf(code, message, details) {
@@ -899,6 +909,16 @@ function hintOf(code, message, reason) {
899
909
  return "\u4ED3\u5E93\u6709\u672A\u63D0\u4EA4\u6539\u52A8\uFF1A\u63D2\u4EF6\u4E0D stash\u3001\u4E0D reset\uFF0C\u8BF7\u5148\u5904\u7406\u3002";
900
910
  case "GITHUB_AUTH":
901
911
  return "GitHub \u8BA4\u8BC1\u5931\u6548\uFF1A\u91CD\u65B0\u8FDE\u63A5\u6216\u66F4\u65B0 PAT\uFF1B\u672C\u5730 commit \u4F1A\u4FDD\u7559\uFF0C\u53EF\u76F4\u63A5\u91CD\u8BD5\u3002";
912
+ case "GITHUB_AUTH_CANCELLED":
913
+ return "\u5DF2\u53D6\u6D88 GitHub \u767B\u5F55\uFF1A\u9700\u8981\u65F6\u5728\u8BBE\u7F6E\u9875\u91CD\u65B0\u70B9\u300C\u7528 GitHub \u767B\u5F55\u300D\u3002";
914
+ case "GITHUB_AUTH_EXPIRED":
915
+ return "GitHub \u767B\u5F55\u8D85\u65F6\uFF1A\u91CD\u65B0\u53D1\u8D77\u767B\u5F55\u5373\u53EF\uFF08\u672A\u5199\u5165\u4EFB\u4F55\u51ED\u636E\uFF09\u3002";
916
+ case "GITHUB_AUTH_STATE":
917
+ return "GitHub \u56DE\u8C03\u6821\u9A8C\u5931\u8D25\uFF08state \u4E0D\u5339\u914D\u6216\u91CD\u590D\u56DE\u8C03\uFF09\uFF1A\u8FD9\u662F\u4E00\u6B21\u5F02\u5E38\u56DE\u8C03\uFF0C\u91CD\u65B0\u53D1\u8D77\u767B\u5F55\u3002";
918
+ case "GITHUB_AUTH_EXCHANGE":
919
+ return "\u7528\u6388\u6743\u7801\u6362\u53D6 GitHub token \u5931\u8D25\uFF1A\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\uFF1B\u82E5\u6301\u7EED\u5931\u8D25\uFF0C\u786E\u8BA4 OAuth App \u7684 client_id \u4E0E\u56DE\u8C03\u5730\u5740\u914D\u7F6E\u4E00\u81F4\u3002";
920
+ case "GITHUB_AUTH_UNAVAILABLE":
921
+ return "\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u6D4F\u89C8\u5668\u767B\u5F55\uFF08\u7F3A\u5C11 authorization \u670D\u52A1\uFF09\uFF1A\u6539\u7528\u300C\u672C\u673A gh\u300D\u6216 PAT \u65B9\u5F0F\u3002";
902
922
  case "NOT_MERGEABLE":
903
923
  return "PR \u65E0\u6CD5\u81EA\u52A8\u5408\u5E76\uFF08\u5E38\u89C1\u4E8E\u5206\u652F\u4FDD\u62A4\uFF09\uFF1APR \u5DF2\u4FDD\u7559\uFF0C\u9700\u4EBA\u5DE5\u5904\u7406\u3002";
904
924
  case "PUSH_FAILED":
@@ -1911,11 +1931,13 @@ async function detectCoexistence(ownPath, options = {}) {
1911
1931
  // src/host/credentials.ts
1912
1932
  var credentials_exports = {};
1913
1933
  __export(credentials_exports, {
1934
+ GITHUB_RECORD_KEY: () => GITHUB_RECORD_KEY,
1914
1935
  credentialServices: () => credentialServices,
1915
1936
  describeRef: () => describeRef,
1916
1937
  resolveGithubToken: () => resolveGithubToken,
1917
1938
  resolveRef: () => resolveRef
1918
1939
  });
1940
+ var GITHUB_RECORD_KEY = "github/default";
1919
1941
  function credentialServices(get) {
1920
1942
  const service = get("credentials");
1921
1943
  return service !== void 0 && typeof service?.resolve === "function" ? service : void 0;
@@ -1951,8 +1973,27 @@ async function resolveRef(services, ref, env = process.env) {
1951
1973
  const envValue = fromEnv(ref, env);
1952
1974
  return envValue === void 0 ? {} : { value: envValue, source: "env" };
1953
1975
  }
1976
+ async function readGithubRecord(services) {
1977
+ if (typeof services?.readRecord !== "function") return void 0;
1978
+ try {
1979
+ return await services.readRecord(GITHUB_RECORD_KEY) ?? void 0;
1980
+ } catch {
1981
+ return void 0;
1982
+ }
1983
+ }
1984
+ function grantToken(record) {
1985
+ if (record === void 0 || record.kind !== "grant") return void 0;
1986
+ const payload = record.payload;
1987
+ const token = payload?.token;
1988
+ return typeof token === "string" && token.trim() !== "" ? token : void 0;
1989
+ }
1954
1990
  async function resolveGithubToken(services, authMode, tokenRef, deps = {}) {
1955
1991
  const env = deps.env ?? process.env;
1992
+ if (authMode === "browser") {
1993
+ const record = await readGithubRecord(services);
1994
+ const token = grantToken(record);
1995
+ if (token !== void 0) return { token, source: "github-browser" };
1996
+ }
1956
1997
  if (authMode === "gh") {
1957
1998
  const execGh = deps.execGh ?? (() => defaultGhToken(deps.freshGh === true));
1958
1999
  try {
@@ -3442,7 +3483,7 @@ async function probeGithubAuth(options) {
3442
3483
  ok: false,
3443
3484
  code: "GITHUB_AUTH",
3444
3485
  source: resolved.source,
3445
- message: options.authMode === "gh" ? "\u6CA1\u6709\u53D6\u5230\u672C\u673A gh \u7684\u767B\u5F55\u6001\uFF1A\u5148\u5728\u7EC8\u7AEF\u6267\u884C `gh auth login`\u3002" : `\u6CA1\u6709\u53D6\u5230\u51ED\u636E\u300C${options.tokenRef}\u300D\uFF1A\u8BF7\u5728\u73AF\u5883\u53D8\u91CF\u6216\u51ED\u636E\u91CC\u914D\u7F6E\u4E00\u4E2A Personal Access Token\u3002`
3486
+ message: options.authMode === "gh" ? "\u6CA1\u6709\u53D6\u5230\u672C\u673A gh \u7684\u767B\u5F55\u6001\uFF1A\u5148\u5728\u7EC8\u7AEF\u6267\u884C `gh auth login`\u3002" : options.authMode === "browser" ? "\u8FD8\u6CA1\u6709\u901A\u8FC7\u6D4F\u89C8\u5668\u767B\u5F55 GitHub\uFF1A\u5728\u8BBE\u7F6E\u9875\u70B9\u300C\u7528 GitHub \u767B\u5F55\u300D\u5B8C\u6210\u6388\u6743\u3002" : `\u6CA1\u6709\u53D6\u5230\u51ED\u636E\u300C${options.tokenRef}\u300D\uFF1A\u8BF7\u5728\u73AF\u5883\u53D8\u91CF\u6216\u51ED\u636E\u91CC\u914D\u7F6E\u4E00\u4E2A Personal Access Token\u3002`
3446
3487
  };
3447
3488
  }
3448
3489
  const doFetch = options.fetchImpl ?? fetch;
@@ -3485,6 +3526,370 @@ async function probeGithubAuth(options) {
3485
3526
  }
3486
3527
  }
3487
3528
 
3529
+ // src/host/github-auth.ts
3530
+ var github_auth_exports = {};
3531
+ __export(github_auth_exports, {
3532
+ CALLBACK_PATH: () => CALLBACK_PATH,
3533
+ DEFAULT_GITHUB_CLIENT_ID: () => DEFAULT_GITHUB_CLIENT_ID,
3534
+ DEFAULT_SCOPE: () => DEFAULT_SCOPE,
3535
+ GITHUB_RECORD_KEY: () => GITHUB_RECORD_KEY2,
3536
+ attemptView: () => attemptView,
3537
+ beginGithubSignIn: () => beginGithubSignIn,
3538
+ buildAuthorizeUrl: () => buildAuthorizeUrl,
3539
+ cancelGithubSignIn: () => cancelGithubSignIn,
3540
+ createPkce: () => createPkce,
3541
+ registerGithubFlow: () => registerGithubFlow,
3542
+ resetAttemptForTest: () => resetAttemptForTest,
3543
+ resolveClientId: () => resolveClientId,
3544
+ verifyState: () => verifyState
3545
+ });
3546
+ import { createHash as createHash3, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
3547
+ var GITHUB_RECORD_KEY2 = "github/default";
3548
+ var CALLBACK_PATH = "/api/morehao/github-trending/oauth/github/callback";
3549
+ var DEFAULT_GITHUB_CLIENT_ID = "Ov23li7rhp0EKoqbkJKY";
3550
+ function resolveClientId(deps) {
3551
+ const fromConfig = deps.clientId;
3552
+ return fromConfig !== void 0 && fromConfig !== "" ? fromConfig : DEFAULT_GITHUB_CLIENT_ID;
3553
+ }
3554
+ var AUTHORIZE_URL = "https://github.com/login/oauth/authorize";
3555
+ var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
3556
+ var DEVICE_CODE_URL = "https://github.com/login/device/code";
3557
+ var GITHUB_USER_URL = "https://api.github.com/user";
3558
+ var DEFAULT_SCOPE = "repo";
3559
+ var DEFAULT_ATTEMPT_TIMEOUT_MS = 10 * 60 * 1e3;
3560
+ var current;
3561
+ function attemptView() {
3562
+ if (current === void 0) return { phase: "idle" };
3563
+ const { controller: _c, done: _d, ...view } = current;
3564
+ return view;
3565
+ }
3566
+ function base64url(buf) {
3567
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
3568
+ }
3569
+ function createPkce() {
3570
+ const verifier = base64url(randomBytes2(32));
3571
+ const challenge = base64url(createHash3("sha256").update(verifier).digest());
3572
+ return { verifier, challenge };
3573
+ }
3574
+ function verifyState(received, expected) {
3575
+ const a = Buffer.from(received);
3576
+ const b = Buffer.from(expected);
3577
+ return a.length === b.length && timingSafeEqual2(a, b);
3578
+ }
3579
+ function buildAuthorizeUrl(params) {
3580
+ const url = new URL(params.authorizeEndpoint);
3581
+ url.searchParams.set("client_id", params.clientId);
3582
+ url.searchParams.set("redirect_uri", params.redirectUri);
3583
+ url.searchParams.set("scope", params.scope);
3584
+ url.searchParams.set("state", params.state);
3585
+ url.searchParams.set("code_challenge", params.challenge);
3586
+ url.searchParams.set("code_challenge_method", "S256");
3587
+ return url.toString();
3588
+ }
3589
+ async function exchangeCode(fetchImpl, endpoint, params, signal) {
3590
+ const response = await fetchImpl(endpoint, {
3591
+ method: "POST",
3592
+ headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
3593
+ body: new URLSearchParams({
3594
+ client_id: params.clientId,
3595
+ code: params.code,
3596
+ code_verifier: params.verifier,
3597
+ redirect_uri: params.redirectUri
3598
+ }).toString(),
3599
+ signal
3600
+ });
3601
+ if (!response.ok) throw new Error(`GitHub access_token HTTP ${response.status}`);
3602
+ const body = await response.json();
3603
+ if (typeof body.access_token !== "string" || body.access_token === "") {
3604
+ throw new Error(`GitHub access_token \u672A\u8FD4\u56DE token${typeof body.error === "string" ? `\uFF08${body.error}\uFF09` : ""}`);
3605
+ }
3606
+ return {
3607
+ token: body.access_token,
3608
+ ...typeof body.token_type === "string" ? { tokenType: body.token_type } : {},
3609
+ ...typeof body.scope === "string" ? { scope: body.scope } : {}
3610
+ };
3611
+ }
3612
+ async function fetchLogin(fetchImpl, endpoint, token, signal) {
3613
+ try {
3614
+ const response = await fetchImpl(endpoint, {
3615
+ headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json", "user-agent": "dsh-github-trending" },
3616
+ signal
3617
+ });
3618
+ if (!response.ok) return void 0;
3619
+ const body = await response.json();
3620
+ return typeof body.login === "string" ? body.login : void 0;
3621
+ } catch {
3622
+ return void 0;
3623
+ }
3624
+ }
3625
+ async function runBrowser(deps, session) {
3626
+ const webServer = deps.get("webServer");
3627
+ if (webServer === void 0 || typeof webServer.register !== "function") {
3628
+ throw new Error("webServer \u670D\u52A1\u4E0D\u53EF\u7528\uFF1A\u6D4F\u89C8\u5668\u767B\u5F55\u9700\u8981\u5B83\u627F\u8F7D OAuth \u56DE\u8C03");
3629
+ }
3630
+ const clientId = resolveClientId(deps);
3631
+ if (clientId === void 0 || clientId === "") throw new Error("\u7F3A\u5C11 GitHub OAuth client_id");
3632
+ const fetchImpl = deps.fetchImpl ?? fetch;
3633
+ const endpoints = normalizeEndpoints(deps.endpoints);
3634
+ const scope = deps.scope ?? DEFAULT_SCOPE;
3635
+ const state = base64url(randomBytes2(32));
3636
+ const { verifier, challenge } = createPkce();
3637
+ let resolveCode;
3638
+ let rejectCode;
3639
+ const codePromise = new Promise((resolve, reject) => {
3640
+ resolveCode = resolve;
3641
+ rejectCode = reject;
3642
+ });
3643
+ codePromise.catch(() => void 0);
3644
+ const dispose = webServer.register({
3645
+ kind: "exact",
3646
+ path: CALLBACK_PATH,
3647
+ handler: (req, res) => {
3648
+ let url;
3649
+ try {
3650
+ url = new URL(req.url ?? "/", "http://127.0.0.1");
3651
+ } catch {
3652
+ res.writeHead(400, { "cache-control": "no-store" }).end();
3653
+ return;
3654
+ }
3655
+ const code = url.searchParams.get("code") ?? "";
3656
+ const gotState = url.searchParams.get("state") ?? "";
3657
+ const single = url.searchParams.getAll("state").length === 1 && url.searchParams.getAll("code").length === 1;
3658
+ const bad = req.method !== "GET" || url.pathname !== CALLBACK_PATH || !single || code === "" || !verifyState(gotState, state);
3659
+ if (bad) {
3660
+ res.writeHead(400, { "cache-control": "no-store" }).end();
3661
+ return;
3662
+ }
3663
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end('<!doctype html><meta charset="utf-8"><title>GitHub</title><p>\u5DF2\u5B8C\u6210\u767B\u5F55\uFF0C\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\u3002</p>');
3664
+ resolveCode(code);
3665
+ }
3666
+ });
3667
+ try {
3668
+ const origin = localOrigin(webServer);
3669
+ const redirectUri = `${origin}${CALLBACK_PATH}`;
3670
+ const authorizeUrl = buildAuthorizeUrl({
3671
+ authorizeEndpoint: endpoints.authorize,
3672
+ clientId,
3673
+ redirectUri,
3674
+ scope,
3675
+ state,
3676
+ challenge
3677
+ });
3678
+ updateCurrent({ phase: "waiting-browser", authorizeUrl, expiresAt: Date.now() + attemptTimeout(deps) });
3679
+ session.notify?.({ message: "Continue in your browser to sign in to GitHub.", url: authorizeUrl });
3680
+ const code = await withAbort(codePromise, session.signal, () => rejectCode(new Error("authorization cancelled")));
3681
+ updateCurrent({ phase: "exchanging" });
3682
+ const exchanged = await exchangeCode(fetchImpl, endpoints.accessToken, { clientId, code, verifier, redirectUri }, session.signal);
3683
+ const login = await fetchLogin(fetchImpl, endpoints.user, exchanged.token, session.signal);
3684
+ const payload = {
3685
+ version: 1,
3686
+ token: exchanged.token,
3687
+ ...login === void 0 ? {} : { login },
3688
+ tokenType: exchanged.tokenType ?? "bearer",
3689
+ scope: exchanged.scope ?? scope
3690
+ };
3691
+ if (typeof session.commit === "function") await session.commit({ kind: "grant", payload });
3692
+ updateCurrent({ phase: "done", ...login === void 0 ? {} : { login } });
3693
+ } finally {
3694
+ dispose();
3695
+ }
3696
+ }
3697
+ async function runDevice(deps, session) {
3698
+ const clientId = resolveClientId(deps);
3699
+ if (clientId === void 0 || clientId === "") throw new Error("\u7F3A\u5C11 GitHub OAuth client_id");
3700
+ const fetchImpl = deps.fetchImpl ?? fetch;
3701
+ const endpoints = normalizeEndpoints(deps.endpoints);
3702
+ const scope = deps.scope ?? DEFAULT_SCOPE;
3703
+ const startResponse = await fetchImpl(endpoints.deviceCode, {
3704
+ method: "POST",
3705
+ headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
3706
+ body: new URLSearchParams({ client_id: clientId, scope }).toString(),
3707
+ signal: session.signal
3708
+ });
3709
+ if (!startResponse.ok) throw new Error(`GitHub device/code HTTP ${startResponse.status}`);
3710
+ const start = await startResponse.json();
3711
+ if (typeof start.device_code !== "string" || typeof start.user_code !== "string" || typeof start.verification_uri !== "string") {
3712
+ throw new Error("GitHub device/code \u8FD4\u56DE\u4E0D\u5B8C\u6574");
3713
+ }
3714
+ const deviceCode = start.device_code;
3715
+ const intervalSec = typeof start.interval === "number" && start.interval > 0 ? start.interval : 5;
3716
+ const expiresInSec = typeof start.expires_in === "number" && start.expires_in > 0 ? start.expires_in : 900;
3717
+ const deadline = Date.now() + expiresInSec * 1e3;
3718
+ updateCurrent({
3719
+ phase: "waiting-device",
3720
+ verificationUri: start.verification_uri,
3721
+ userCode: start.user_code,
3722
+ expiresAt: deadline
3723
+ });
3724
+ session.notify?.({
3725
+ message: "Enter this code on the verification page to finish signing in.",
3726
+ url: start.verification_uri,
3727
+ code: start.user_code
3728
+ });
3729
+ let waitMs = intervalSec * 1e3;
3730
+ while (Date.now() < deadline) {
3731
+ await sleep(waitMs, session.signal);
3732
+ const pollResponse = await fetchImpl(endpoints.accessToken, {
3733
+ method: "POST",
3734
+ headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
3735
+ body: new URLSearchParams({
3736
+ client_id: clientId,
3737
+ device_code: deviceCode,
3738
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
3739
+ }).toString(),
3740
+ signal: session.signal
3741
+ });
3742
+ if (!pollResponse.ok) throw new Error(`GitHub access_token HTTP ${pollResponse.status}`);
3743
+ const poll = await pollResponse.json();
3744
+ if (typeof poll.access_token === "string" && poll.access_token !== "") {
3745
+ updateCurrent({ phase: "exchanging" });
3746
+ const login = await fetchLogin(fetchImpl, endpoints.user, poll.access_token, session.signal);
3747
+ const payload = {
3748
+ version: 1,
3749
+ token: poll.access_token,
3750
+ ...login === void 0 ? {} : { login },
3751
+ tokenType: typeof poll.token_type === "string" ? poll.token_type : "bearer",
3752
+ scope: typeof poll.scope === "string" ? poll.scope : scope
3753
+ };
3754
+ if (typeof session.commit === "function") await session.commit({ kind: "grant", payload });
3755
+ updateCurrent({ phase: "done", ...login === void 0 ? {} : { login } });
3756
+ return;
3757
+ }
3758
+ if (poll.error === "authorization_pending") continue;
3759
+ if (poll.error === "slow_down") {
3760
+ waitMs += (typeof poll.interval === "number" && poll.interval > 0 ? poll.interval : 5) * 1e3;
3761
+ continue;
3762
+ }
3763
+ throw new Error(`GitHub device flow \u7EC8\u6B62\uFF1A${typeof poll.error === "string" ? poll.error : "unknown"}`);
3764
+ }
3765
+ throw new Error("GitHub device flow \u8D85\u65F6");
3766
+ }
3767
+ function registerGithubFlow(deps) {
3768
+ const authorization = deps.get("authorization");
3769
+ if (authorization === void 0 || typeof authorization.registerFlow !== "function") {
3770
+ deps.logger?.("auth.flow.skipped", { reason: "no-authorization-service" });
3771
+ return;
3772
+ }
3773
+ authorization.registerFlow({
3774
+ key: GITHUB_RECORD_KEY2,
3775
+ label: "GitHub",
3776
+ methods: [
3777
+ { id: "browser", label: "Sign in with GitHub" },
3778
+ { id: "device", label: "Use a device code" }
3779
+ ],
3780
+ run: (session) => session.method === "device" ? runDevice(deps, session) : runBrowser(deps, session)
3781
+ });
3782
+ deps.logger?.("auth.flow.registered", { key: GITHUB_RECORD_KEY2 });
3783
+ }
3784
+ async function beginGithubSignIn(deps, method = "browser") {
3785
+ const authorization = deps.get("authorization");
3786
+ if (authorization === void 0 || typeof authorization.begin !== "function") {
3787
+ throw new Error("authorization \u670D\u52A1\u4E0D\u53EF\u7528\uFF1A\u65E0\u6CD5\u8FDB\u884C\u6D4F\u89C8\u5668\u767B\u5F55");
3788
+ }
3789
+ if (current !== void 0 && (current.phase === "waiting-browser" || current.phase === "waiting-device" || current.phase === "exchanging")) {
3790
+ throw new Error("already-in-flight");
3791
+ }
3792
+ const controller = new AbortController();
3793
+ const timer = setTimeout(() => controller.abort(), attemptTimeout(deps));
3794
+ const attempt = { phase: "idle", controller, done: Promise.resolve() };
3795
+ current = attempt;
3796
+ attempt.done = authorization.begin({
3797
+ key: GITHUB_RECORD_KEY2,
3798
+ method,
3799
+ signal: controller.signal,
3800
+ // 浏览器往返靠 authorizeUrl + 回调;notify 只更新内存视图,prompt 不支持(拒绝)。
3801
+ interaction: {
3802
+ notify: (notice) => {
3803
+ if (notice.code !== void 0 && notice.url !== void 0) {
3804
+ updateCurrent({ phase: "waiting-device", verificationUri: notice.url, userCode: notice.code });
3805
+ } else if (notice.url !== void 0) {
3806
+ updateCurrent({ phase: "waiting-browser", authorizeUrl: notice.url });
3807
+ }
3808
+ },
3809
+ prompt: () => Promise.reject(new Error("interactive prompt unsupported in this surface"))
3810
+ }
3811
+ }).then((outcome) => {
3812
+ if (outcome.status === "cancelled") updateCurrent({ phase: "idle" });
3813
+ }).catch((error) => {
3814
+ updateCurrent({ phase: "error", error: error instanceof Error ? error.message : String(error) });
3815
+ }).finally(() => clearTimeout(timer));
3816
+ await Promise.resolve();
3817
+ return attemptView();
3818
+ }
3819
+ function cancelGithubSignIn(deps) {
3820
+ const authorization = deps.get("authorization");
3821
+ if (authorization !== void 0 && typeof authorization.cancel === "function") {
3822
+ authorization.cancel(GITHUB_RECORD_KEY2);
3823
+ }
3824
+ if (current !== void 0) current.controller.abort();
3825
+ }
3826
+ function resetAttemptForTest() {
3827
+ current = void 0;
3828
+ }
3829
+ function normalizeEndpoints(endpoints = {}) {
3830
+ return {
3831
+ authorize: endpoints?.authorize ?? AUTHORIZE_URL,
3832
+ accessToken: endpoints?.accessToken ?? ACCESS_TOKEN_URL,
3833
+ deviceCode: endpoints?.deviceCode ?? DEVICE_CODE_URL,
3834
+ user: endpoints?.user ?? GITHUB_USER_URL
3835
+ };
3836
+ }
3837
+ function attemptTimeout(deps) {
3838
+ return typeof deps.attemptTimeoutMs === "number" && deps.attemptTimeoutMs > 0 ? deps.attemptTimeoutMs : DEFAULT_ATTEMPT_TIMEOUT_MS;
3839
+ }
3840
+ function localOrigin(webServer) {
3841
+ const port = webServer.port;
3842
+ if (typeof port !== "number" || port <= 0) throw new Error("webServer \u5C1A\u672A\u76D1\u542C\uFF0C\u65E0\u6CD5\u786E\u5B9A\u56DE\u8C03\u5730\u5740");
3843
+ const host = typeof webServer.host === "string" && webServer.host !== "" ? webServer.host : "127.0.0.1";
3844
+ const loopback = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
3845
+ return `http://${loopback}:${port}`;
3846
+ }
3847
+ function withAbort(promise, signal, onAbort) {
3848
+ if (signal === void 0) return promise;
3849
+ if (signal.aborted) {
3850
+ onAbort();
3851
+ return Promise.reject(new Error("authorization cancelled"));
3852
+ }
3853
+ return new Promise((resolve, reject) => {
3854
+ const abort = () => {
3855
+ onAbort();
3856
+ reject(new Error("authorization cancelled"));
3857
+ };
3858
+ signal.addEventListener("abort", abort, { once: true });
3859
+ promise.then(
3860
+ (value) => {
3861
+ signal.removeEventListener("abort", abort);
3862
+ resolve(value);
3863
+ },
3864
+ (error) => {
3865
+ signal.removeEventListener("abort", abort);
3866
+ reject(error);
3867
+ }
3868
+ );
3869
+ });
3870
+ }
3871
+ function sleep(ms, signal) {
3872
+ return new Promise((resolve, reject) => {
3873
+ if (signal?.aborted === true) {
3874
+ reject(new Error("authorization cancelled"));
3875
+ return;
3876
+ }
3877
+ const timer = setTimeout(() => {
3878
+ signal?.removeEventListener("abort", abort);
3879
+ resolve();
3880
+ }, ms);
3881
+ const abort = () => {
3882
+ clearTimeout(timer);
3883
+ reject(new Error("authorization cancelled"));
3884
+ };
3885
+ signal?.addEventListener("abort", abort, { once: true });
3886
+ });
3887
+ }
3888
+ function updateCurrent(patch) {
3889
+ if (current === void 0) return;
3890
+ Object.assign(current, patch);
3891
+ }
3892
+
3488
3893
  // src/host/import-cli.ts
3489
3894
  var import_cli_exports = {};
3490
3895
  __export(import_cli_exports, {
@@ -3532,7 +3937,7 @@ function scalar(raw) {
3532
3937
  }
3533
3938
  function parseSimpleYaml(text) {
3534
3939
  const root = {};
3535
- let current;
3940
+ let current2;
3536
3941
  let currentIndent = -1;
3537
3942
  for (const rawLine of text.split("\n")) {
3538
3943
  const line = rawLine.replace(/\r$/, "");
@@ -3546,21 +3951,21 @@ function parseSimpleYaml(text) {
3546
3951
  if (rest.trim() === "") {
3547
3952
  const child = {};
3548
3953
  root[key] = child;
3549
- current = child;
3954
+ current2 = child;
3550
3955
  currentIndent = indent;
3551
3956
  continue;
3552
3957
  }
3553
3958
  if (indent === 0) {
3554
3959
  root[key] = scalar(rest);
3555
- current = void 0;
3960
+ current2 = void 0;
3556
3961
  currentIndent = -1;
3557
3962
  continue;
3558
3963
  }
3559
- if (current === void 0 || indent <= currentIndent) {
3964
+ if (current2 === void 0 || indent <= currentIndent) {
3560
3965
  root[key] = scalar(rest);
3561
3966
  continue;
3562
3967
  }
3563
- current[key] = scalar(rest);
3968
+ current2[key] = scalar(rest);
3564
3969
  }
3565
3970
  return root;
3566
3971
  }
@@ -3983,15 +4388,27 @@ import { PluginUpdateError } from "@morehao/dsh-plugin-kit/update";
3983
4388
  var ROUTE_PREFIX = "/api/morehao/github-trending";
3984
4389
  var HEADER = "x-dsh-github-trending";
3985
4390
  var MAX_BODY2 = 512 * 1024;
4391
+ function isAuthorizationAvailable(get) {
4392
+ const authorization = get("authorization");
4393
+ return authorization !== void 0 && typeof authorization.begin === "function";
4394
+ }
4395
+ function githubAuthDeps(deps) {
4396
+ return {
4397
+ get: deps.get,
4398
+ ...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl },
4399
+ ...deps.githubClientId === void 0 ? {} : { clientId: deps.githubClientId },
4400
+ ...deps.logger === void 0 ? {} : { logger: deps.logger }
4401
+ };
4402
+ }
3986
4403
  function createRoutes(deps) {
3987
4404
  const logger = deps.logger;
3988
- let current = { method: "GET", pathname: "/" };
4405
+ let current2 = { method: "GET", pathname: "/" };
3989
4406
  const handler = createRouteHandler({
3990
4407
  prefix: ROUTE_PREFIX,
3991
4408
  header: HEADER,
3992
4409
  maxBodyBytes: MAX_BODY2,
3993
4410
  dispatch: async ({ method, pathname, url, body }) => {
3994
- current = { method, pathname };
4411
+ current2 = { method, pathname };
3995
4412
  return dispatch(method, pathname, url, body, deps);
3996
4413
  },
3997
4414
  // 错误码翻译留在插件侧(dispatcher 不做映射):保持与原实现的响应形状逐字一致。
@@ -4011,7 +4428,7 @@ function createRoutes(deps) {
4011
4428
  }
4012
4429
  const code = error instanceof IpcError ? error.code : "INTERNAL";
4013
4430
  const message = redact(error instanceof Error ? error.message : String(error));
4014
- logger?.("route.error", { pathname: current.pathname, method: current.method, code, message });
4431
+ logger?.("route.error", { pathname: current2.pathname, method: current2.method, code, message });
4015
4432
  const status = code === "CONFIG_INVALID" ? (
4016
4433
  // 「已有一次运行在进行中」是并发冲突而非参数错误:用 409,前端据此禁用按钮并提示。
4017
4434
  message.includes("\u8FD0\u884C\u5728\u8FDB\u884C\u4E2D") ? 409 : 400
@@ -4165,6 +4582,26 @@ async function dispatch(method, pathname, url, body, deps) {
4165
4582
  });
4166
4583
  return { github: probe };
4167
4584
  }
4585
+ if (method === "POST" && pathname === "/github/browser/begin") {
4586
+ const method2 = body.method === "device" ? "device" : "browser";
4587
+ try {
4588
+ const view = await beginGithubSignIn(githubAuthDeps(deps), method2);
4589
+ return { attempt: view };
4590
+ } catch (error) {
4591
+ const message = error instanceof Error ? error.message : String(error);
4592
+ if (message === "already-in-flight") {
4593
+ return { attempt: attemptView(), alreadyInFlight: true };
4594
+ }
4595
+ throw new IpcError("GITHUB_AUTH_UNAVAILABLE", redact(message));
4596
+ }
4597
+ }
4598
+ if (method === "GET" && pathname === "/github/browser/status") {
4599
+ return { attempt: attemptView() };
4600
+ }
4601
+ if (method === "POST" && pathname === "/github/browser/cancel") {
4602
+ cancelGithubSignIn(githubAuthDeps(deps));
4603
+ return { attempt: attemptView() };
4604
+ }
4168
4605
  if (method === "GET" && pathname === "/quickfill") {
4169
4606
  const candidates = await listProviderCandidates({
4170
4607
  llm: llmServices(deps.get),
@@ -4293,7 +4730,11 @@ async function statePayload(deps, settings) {
4293
4730
  github: {
4294
4731
  authMode: settings.github.authMode,
4295
4732
  configured: github.token !== void 0,
4296
- source: github.source
4733
+ source: github.source,
4734
+ /** 浏览器登录 attempt 视图(idle / waiting-browser / waiting-device / exchanging / done / error)。 */
4735
+ attempt: attemptView(),
4736
+ /** 浏览器登录是否可用:核心提供了 authorization 服务即可(client_id 有内置默认)。 */
4737
+ browserAvailable: isAuthorizationAvailable(deps.get)
4297
4738
  }
4298
4739
  };
4299
4740
  }
@@ -4572,6 +5013,8 @@ export {
4572
5013
  readPluginInfo,
4573
5014
  plugin_info_exports,
4574
5015
  github_exports,
5016
+ registerGithubFlow,
5017
+ github_auth_exports,
4575
5018
  import_cli_exports,
4576
5019
  pending_pr_exports,
4577
5020
  preflight_exports,
package/lib/index.d.ts CHANGED
@@ -24,6 +24,7 @@ declare const VERSION: string;
24
24
  declare function apply(ctx: PluginContext, config?: {
25
25
  stateDir?: string;
26
26
  corePath?: string;
27
+ githubClientId?: string;
27
28
  }): void;
28
29
  declare const _default: {
29
30
  name: string;
package/lib/index.js CHANGED
@@ -7,8 +7,9 @@ import {
7
7
  createTrendingHistoryTool,
8
8
  createTrendingRunTool,
9
9
  createUpdateService,
10
- readPluginInfo
11
- } from "./chunk-YJ5GF3MW.js";
10
+ readPluginInfo,
11
+ registerGithubFlow
12
+ } from "./chunk-LLTW4YHJ.js";
12
13
 
13
14
  // src/index.ts
14
15
  var name = "dsh-github-trending";
@@ -63,6 +64,11 @@ function apply(ctx, config = {}) {
63
64
  onError: (error) => ctx.logger?.warn?.(`[dsh-github-trending] \u5B9A\u65F6\u8FD0\u884C\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`)
64
65
  });
65
66
  const pluginInfo = readPluginInfo();
67
+ registerGithubFlow({
68
+ get,
69
+ logger: log,
70
+ ...config.githubClientId === void 0 ? {} : { clientId: config.githubClientId }
71
+ });
66
72
  const update = createUpdateService({
67
73
  get,
68
74
  env,
@@ -83,6 +89,7 @@ function apply(ctx, config = {}) {
83
89
  version: VERSION,
84
90
  pluginName: pluginInfo.name,
85
91
  ...pluginInfo.repository === void 0 ? {} : { repository: pluginInfo.repository },
92
+ ...config.githubClientId === void 0 ? {} : { githubClientId: config.githubClientId },
86
93
  logger: log
87
94
  });
88
95
  const register = () => {
package/lib/internal.d.ts CHANGED
@@ -17,7 +17,7 @@ import { Updater, PluginMeta, Logger } from '@morehao/dsh-plugin-kit/update';
17
17
  */
18
18
 
19
19
  /** 插件在核心之外新增的码。前缀统一避免与核心码冲突。 */
20
- declare const PLUGIN_CODES: readonly ["CORE_UNAVAILABLE", "CORE_INCOMPATIBLE", "CORE_SPAWN_FAILED", "CORE_TIMEOUT", "CORE_CANCELLED", "CORE_CONFIG_CHANNEL_FAILED", "CORE_NOT_DOWNLOADED", "CORE_CHECKSUM_MISMATCH", "IPC_INVALID", "MODEL_KEY_MISSING", "MODEL_NOT_FOUND", "MODEL_PROVIDER_UNSUPPORTED", "GITHUB_AUTH", "PUSH_FAILED", "REPO_DIRTY", "SUBSCRIBE_FAILED"];
20
+ declare const PLUGIN_CODES: readonly ["CORE_UNAVAILABLE", "CORE_INCOMPATIBLE", "CORE_SPAWN_FAILED", "CORE_TIMEOUT", "CORE_CANCELLED", "CORE_CONFIG_CHANNEL_FAILED", "CORE_NOT_DOWNLOADED", "CORE_CHECKSUM_MISMATCH", "IPC_INVALID", "MODEL_KEY_MISSING", "MODEL_NOT_FOUND", "MODEL_PROVIDER_UNSUPPORTED", "GITHUB_AUTH", "GITHUB_AUTH_CANCELLED", "GITHUB_AUTH_EXPIRED", "GITHUB_AUTH_STATE", "GITHUB_AUTH_EXCHANGE", "GITHUB_AUTH_UNAVAILABLE", "PUSH_FAILED", "REPO_DIRTY", "SUBSCRIBE_FAILED"];
21
21
  type PluginCode = (typeof PLUGIN_CODES)[number];
22
22
  type AnyCode = ErrorCode | PluginCode;
23
23
  interface ClassifiedError {
@@ -211,8 +211,11 @@ declare namespace ipc {
211
211
 
212
212
  type Provider = string;
213
213
  type CommitMode = 'branch' | 'direct';
214
- /** 只保留两种真正可用的方式:本机 `gh` 登录态、手动 PAT。历史的 `device` 在归一化时迁移为 `gh`。 */
215
- type AuthMode = 'pat' | 'gh';
214
+ /**
215
+ * 三种可用方式:本机 `gh` 登录态、手动 PAT、浏览器 GitHub 登录(读核心凭据记录)。
216
+ * 历史的 `device` 在归一化时迁移为 `gh`;`browser` 无记录时会降级到 ref/env 兜底。
217
+ */
218
+ type AuthMode = 'pat' | 'gh' | 'browser';
216
219
  /**
217
220
  * 榜单周期(core 0.4.0 的 `--period` 三取值)。
218
221
  * `trending.since` 复用同一枚举:单次运行「抓取窗口 = 标注周期」由核心保证
@@ -1084,16 +1087,27 @@ interface ResolvedSecret {
1084
1087
  value?: string;
1085
1088
  source?: string;
1086
1089
  }
1090
+ interface CredentialRecord {
1091
+ kind: string;
1092
+ payload: unknown;
1093
+ }
1087
1094
  interface CredentialServices {
1088
1095
  describe?: (ref: string) => Promise<CredentialDescription> | CredentialDescription;
1089
1096
  resolve?: (ref: string) => Promise<ResolvedSecret | undefined | null> | ResolvedSecret | undefined | null;
1090
1097
  set?: (ref: string, value: string) => Promise<unknown> | unknown;
1098
+ /**
1099
+ * record 半边(`<scope>/<id>` 键):只读,用于 `authMode=browser` 读 GitHub 登录记录。
1100
+ * 由核心 `dsh-credentials-local` 的同一对象提供(与 ref 半边并存);缺失时 browser 模式退回 ref/env。
1101
+ */
1102
+ readRecord?: (key: string) => Promise<CredentialRecord | undefined> | CredentialRecord | undefined;
1091
1103
  }
1104
+ /** 浏览器登录写入核心凭据记录的键(与核心 `@deepseek-ai/dsh-github-auth` 提案同名同构)。 */
1105
+ declare const GITHUB_RECORD_KEY$1 = "github/default";
1092
1106
  declare function credentialServices(get: (name: string) => unknown): CredentialServices | undefined;
1093
1107
  declare function describeRef(services: CredentialServices | undefined, ref: string, env?: NodeJS.ProcessEnv): Promise<CredentialDescription>;
1094
1108
  declare function resolveRef(services: CredentialServices | undefined, ref: string, env?: NodeJS.ProcessEnv): Promise<ResolvedSecret>;
1095
- /** GitHub token:按 authMode 取,`gh` 模式读本机 gh 的登录态,`pat` 读凭据/环境变量。 */
1096
- declare function resolveGithubToken(services: CredentialServices | undefined, authMode: 'pat' | 'gh', tokenRef: string, deps?: {
1109
+ /** GitHub token:按 authMode 取,`gh` 读本机 gh 登录态,`browser` 读核心凭据记录,`pat` 读 ref/env。 */
1110
+ declare function resolveGithubToken(services: CredentialServices | undefined, authMode: 'pat' | 'gh' | 'browser', tokenRef: string, deps?: {
1097
1111
  env?: NodeJS.ProcessEnv;
1098
1112
  execGh?: () => Promise<string | undefined>;
1099
1113
  freshGh?: boolean;
@@ -1103,6 +1117,7 @@ declare function resolveGithubToken(services: CredentialServices | undefined, au
1103
1117
  }>;
1104
1118
 
1105
1119
  type credentials_CredentialDescription = CredentialDescription;
1120
+ type credentials_CredentialRecord = CredentialRecord;
1106
1121
  type credentials_CredentialServices = CredentialServices;
1107
1122
  type credentials_ResolvedSecret = ResolvedSecret;
1108
1123
  declare const credentials_credentialServices: typeof credentialServices;
@@ -1110,7 +1125,7 @@ declare const credentials_describeRef: typeof describeRef;
1110
1125
  declare const credentials_resolveGithubToken: typeof resolveGithubToken;
1111
1126
  declare const credentials_resolveRef: typeof resolveRef;
1112
1127
  declare namespace credentials {
1113
- export { type credentials_CredentialDescription as CredentialDescription, type credentials_CredentialServices as CredentialServices, type credentials_ResolvedSecret as ResolvedSecret, credentials_credentialServices as credentialServices, credentials_describeRef as describeRef, credentials_resolveGithubToken as resolveGithubToken, credentials_resolveRef as resolveRef };
1128
+ export { type credentials_CredentialDescription as CredentialDescription, type credentials_CredentialRecord as CredentialRecord, type credentials_CredentialServices as CredentialServices, GITHUB_RECORD_KEY$1 as GITHUB_RECORD_KEY, type credentials_ResolvedSecret as ResolvedSecret, credentials_credentialServices as credentialServices, credentials_describeRef as describeRef, credentials_resolveGithubToken as resolveGithubToken, credentials_resolveRef as resolveRef };
1114
1129
  }
1115
1130
 
1116
1131
  /**
@@ -1130,7 +1145,7 @@ interface GithubProbeResult {
1130
1145
  }
1131
1146
  declare function probeGithubAuth(options: {
1132
1147
  credentials: CredentialServices | undefined;
1133
- authMode: 'pat' | 'gh';
1148
+ authMode: 'pat' | 'gh' | 'browser';
1134
1149
  tokenRef: string;
1135
1150
  env?: NodeJS.ProcessEnv;
1136
1151
  fetchImpl?: typeof fetch;
@@ -1142,6 +1157,153 @@ declare namespace github {
1142
1157
  export { type github_GithubProbeResult as GithubProbeResult, github_probeGithubAuth as probeGithubAuth };
1143
1158
  }
1144
1159
 
1160
+ /** 与核心 `@deepseek-ai/dsh-github-auth` 提案保持同 key;迁移时消费端无需改。 */
1161
+ 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
+ /**
1165
+ * 内置的 GitHub OAuth App 公开 client_id(**非机密**:OAuth 的 client_id 设计上就是公开的,
1166
+ * 会出现在授权 URL 里,与 `gh`/VS Code 内置自己的 client_id 同理)。
1167
+ * 纯 PKCE public client,无需 client_secret。用户/部署可在 `apply(config.githubClientId)` 覆盖。
1168
+ *
1169
+ * OAuth App 侧登记的回调:`http://127.0.0.1/api/morehao/github-trending/oauth/github/callback`
1170
+ * (loopback 按 RFC 8252 忽略端口,运行时随机端口可正常回跳)。
1171
+ */
1172
+ declare const DEFAULT_GITHUB_CLIENT_ID = "Ov23li7rhp0EKoqbkJKY";
1173
+ /** 解析生效的 client_id:显式 config 优先,否则用内置默认。 */
1174
+ declare function resolveClientId(deps: GithubAuthDeps): string | undefined;
1175
+ /** 本插件运行需要 push / 建 PR,`read:user` 不够;一次授权到 `repo`。 */
1176
+ declare const DEFAULT_SCOPE = "repo";
1177
+ interface AuthorizationFlow {
1178
+ key: string;
1179
+ label: string;
1180
+ methods: Array<{
1181
+ id: string;
1182
+ label: string;
1183
+ }>;
1184
+ run: (session: AuthorizationSessionLike) => Promise<void> | void;
1185
+ }
1186
+ interface AuthorizationSessionLike {
1187
+ method?: string;
1188
+ signal?: AbortSignal;
1189
+ notify?: (notice: {
1190
+ message: string;
1191
+ url?: string;
1192
+ code?: string;
1193
+ }) => void;
1194
+ prompt?: (prompt: unknown) => Promise<unknown>;
1195
+ commit?: (record: {
1196
+ kind: 'grant';
1197
+ payload: unknown;
1198
+ }) => Promise<unknown>;
1199
+ }
1200
+ interface AuthorizationLike {
1201
+ registerFlow?: (flow: AuthorizationFlow) => unknown;
1202
+ begin?: (request: unknown) => Promise<{
1203
+ status: 'authorized' | 'cancelled';
1204
+ }>;
1205
+ cancel?: (key: string) => void;
1206
+ }
1207
+ interface WebServerLike {
1208
+ register?: (route: {
1209
+ kind: 'exact';
1210
+ path: string;
1211
+ handler: (req: any, res: any) => void;
1212
+ }) => () => void;
1213
+ port?: number;
1214
+ host?: string;
1215
+ }
1216
+ type FetchLike = typeof fetch;
1217
+ interface GithubAuthDeps {
1218
+ get: (name: string) => unknown;
1219
+ fetchImpl?: FetchLike;
1220
+ logger?: (event: string, data?: Record<string, unknown>) => void;
1221
+ /** 覆盖端点(测试用);生产留空走 GitHub 官方。 */
1222
+ endpoints?: {
1223
+ authorize?: string;
1224
+ accessToken?: string;
1225
+ deviceCode?: string;
1226
+ user?: string;
1227
+ };
1228
+ /** GitHub OAuth App 的 public client_id(无需 secret)。 */
1229
+ clientId?: string;
1230
+ scope?: string;
1231
+ attemptTimeoutMs?: number;
1232
+ }
1233
+ type GithubAttemptPhase = 'idle' | 'waiting-browser' | 'waiting-device' | 'exchanging' | 'done' | 'error';
1234
+ interface GithubAttemptView {
1235
+ phase: GithubAttemptPhase;
1236
+ authorizeUrl?: string;
1237
+ verificationUri?: string;
1238
+ userCode?: string;
1239
+ expiresAt?: number;
1240
+ login?: string;
1241
+ error?: string;
1242
+ }
1243
+ declare function attemptView(): GithubAttemptView;
1244
+ /** PKCE:返回 verifier 与 S256 challenge。 */
1245
+ declare function createPkce(): {
1246
+ verifier: string;
1247
+ challenge: string;
1248
+ };
1249
+ /** 常量时间比较 state(长度不等直接 false,避免 timingSafeEqual 抛错)。 */
1250
+ declare function verifyState(received: string, expected: string): boolean;
1251
+ /** 拼授权页 URL。 */
1252
+ declare function buildAuthorizeUrl(params: {
1253
+ authorizeEndpoint: string;
1254
+ clientId: string;
1255
+ redirectUri: string;
1256
+ scope: string;
1257
+ state: string;
1258
+ challenge: string;
1259
+ }): string;
1260
+ interface GithubGrantPayload {
1261
+ version: 1;
1262
+ token: string;
1263
+ login?: string;
1264
+ tokenType?: string;
1265
+ scope?: string;
1266
+ }
1267
+ /**
1268
+ * 向 `ctx.authorization` 注册 GitHub flow(若核心没有该服务则降级 no-op)。
1269
+ * 在 `apply()` 里调用一次。
1270
+ */
1271
+ declare function registerGithubFlow(deps: GithubAuthDeps): void;
1272
+ /**
1273
+ * 启动一次浏览器登录 attempt(进程内 `authorization.begin`,不阻塞请求)。
1274
+ * @returns 当前 attempt 视图(含 authorizeUrl,供前端打开)。二次调用在已有进行中时抛错。
1275
+ */
1276
+ declare function beginGithubSignIn(deps: GithubAuthDeps, method?: 'browser' | 'device'): Promise<GithubAttemptView>;
1277
+ /** 取消进行中的 attempt。 */
1278
+ declare function cancelGithubSignIn(deps: GithubAuthDeps): void;
1279
+ /** 仅测试用:清空内存 attempt。 */
1280
+ declare function resetAttemptForTest(): void;
1281
+
1282
+ type githubAuth_AuthorizationFlow = AuthorizationFlow;
1283
+ type githubAuth_AuthorizationLike = AuthorizationLike;
1284
+ type githubAuth_AuthorizationSessionLike = AuthorizationSessionLike;
1285
+ declare const githubAuth_CALLBACK_PATH: typeof CALLBACK_PATH;
1286
+ declare const githubAuth_DEFAULT_GITHUB_CLIENT_ID: typeof DEFAULT_GITHUB_CLIENT_ID;
1287
+ declare const githubAuth_DEFAULT_SCOPE: typeof DEFAULT_SCOPE;
1288
+ declare const githubAuth_GITHUB_RECORD_KEY: typeof GITHUB_RECORD_KEY;
1289
+ type githubAuth_GithubAttemptPhase = GithubAttemptPhase;
1290
+ type githubAuth_GithubAttemptView = GithubAttemptView;
1291
+ type githubAuth_GithubAuthDeps = GithubAuthDeps;
1292
+ type githubAuth_GithubGrantPayload = GithubGrantPayload;
1293
+ type githubAuth_WebServerLike = WebServerLike;
1294
+ declare const githubAuth_attemptView: typeof attemptView;
1295
+ declare const githubAuth_beginGithubSignIn: typeof beginGithubSignIn;
1296
+ declare const githubAuth_buildAuthorizeUrl: typeof buildAuthorizeUrl;
1297
+ declare const githubAuth_cancelGithubSignIn: typeof cancelGithubSignIn;
1298
+ declare const githubAuth_createPkce: typeof createPkce;
1299
+ declare const githubAuth_registerGithubFlow: typeof registerGithubFlow;
1300
+ declare const githubAuth_resetAttemptForTest: typeof resetAttemptForTest;
1301
+ declare const githubAuth_resolveClientId: typeof resolveClientId;
1302
+ declare const githubAuth_verifyState: typeof verifyState;
1303
+ declare namespace githubAuth {
1304
+ 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_registerGithubFlow as registerGithubFlow, githubAuth_resetAttemptForTest as resetAttemptForTest, githubAuth_resolveClientId as resolveClientId, githubAuth_verifyState as verifyState };
1305
+ }
1306
+
1145
1307
  interface DefaultBranchResult {
1146
1308
  branch?: string;
1147
1309
  /** `origin-head` = 远端默认分支;`head` = 本地当前分支兜底;`none` = 读不到。 */
@@ -1823,6 +1985,8 @@ interface RouteDeps {
1823
1985
  /** 包名与仓库地址都来自 package.json,由 src/index.ts 传入。 */
1824
1986
  pluginName?: string;
1825
1987
  repository?: string;
1988
+ /** 浏览器 GitHub 登录用的 public client_id(由 apply 的 config 传入;缺省时按钮会提示未配置)。 */
1989
+ githubClientId?: string;
1826
1990
  logger?: (event: string, data?: Record<string, unknown>) => void;
1827
1991
  }
1828
1992
  declare function createRoutes(deps: RouteDeps): (request: http.IncomingMessage, response: http.ServerResponse) => Promise<void>;
@@ -2145,4 +2309,4 @@ declare namespace tools {
2145
2309
  export { tools_createTrendingHistoryTool as createTrendingHistoryTool, tools_createTrendingRunTool as createTrendingRunTool };
2146
2310
  }
2147
2311
 
2148
- export { channel, config, core, credentials, errors, github, handshake$1 as handshake, importCli, ipc, locate, paths, pendingPr, pluginInfo, preflight, proxy, quickfill, release, repo, routes, run, runner, schedule, state, subscribe, tools, update as updateService, version };
2312
+ export { channel, config, core, credentials, errors, github, githubAuth, handshake$1 as handshake, importCli, ipc, locate, paths, pendingPr, pluginInfo, preflight, proxy, quickfill, release, repo, routes, run, runner, schedule, state, subscribe, tools, update as updateService, version };
package/lib/internal.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  core_exports,
5
5
  credentials_exports,
6
6
  errors_exports,
7
+ github_auth_exports,
7
8
  github_exports,
8
9
  handshake_exports,
9
10
  import_cli_exports,
@@ -26,7 +27,7 @@ import {
26
27
  tools_exports,
27
28
  update_exports,
28
29
  version_exports
29
- } from "./chunk-YJ5GF3MW.js";
30
+ } from "./chunk-LLTW4YHJ.js";
30
31
 
31
32
  // src/internal.ts
32
33
  import * as exec from "@morehao/dsh-plugin-kit/exec";
@@ -38,6 +39,7 @@ export {
38
39
  errors_exports as errors,
39
40
  exec,
40
41
  github_exports as github,
42
+ github_auth_exports as githubAuth,
41
43
  handshake_exports as handshake,
42
44
  import_cli_exports as importCli,
43
45
  ipc_exports as ipc,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morehao/dsh-github-trending",
3
- "version": "0.7.11",
3
+ "version": "0.8.1",
4
4
  "description": "GitHub 趋势日报的 DeepSeek Harness 插件:按日 / 周 / 月周期定时 spawn 本地 trendradar 核心,抓取 Trending、生成中文简介与周期总结,并可选把订阅仓库的新 Release 合成中文摘要,一起提交到你的仓库、自动开 PR 合并;内置概览 / 榜单 / 运行记录 / 订阅 / 设置工作台。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",