@morehao/dsh-github-trending 0.7.10 → 0.8.0

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.
@@ -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,363 @@ 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_SCOPE: () => DEFAULT_SCOPE,
3534
+ GITHUB_RECORD_KEY: () => GITHUB_RECORD_KEY2,
3535
+ attemptView: () => attemptView,
3536
+ beginGithubSignIn: () => beginGithubSignIn,
3537
+ buildAuthorizeUrl: () => buildAuthorizeUrl,
3538
+ cancelGithubSignIn: () => cancelGithubSignIn,
3539
+ createPkce: () => createPkce,
3540
+ registerGithubFlow: () => registerGithubFlow,
3541
+ resetAttemptForTest: () => resetAttemptForTest,
3542
+ verifyState: () => verifyState
3543
+ });
3544
+ import { createHash as createHash3, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
3545
+ var GITHUB_RECORD_KEY2 = "github/default";
3546
+ var CALLBACK_PATH = "/api/morehao/github-trending/oauth/github/callback";
3547
+ var AUTHORIZE_URL = "https://github.com/login/oauth/authorize";
3548
+ var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
3549
+ var DEVICE_CODE_URL = "https://github.com/login/device/code";
3550
+ var GITHUB_USER_URL = "https://api.github.com/user";
3551
+ var DEFAULT_SCOPE = "repo";
3552
+ var DEFAULT_ATTEMPT_TIMEOUT_MS = 10 * 60 * 1e3;
3553
+ var current;
3554
+ function attemptView() {
3555
+ if (current === void 0) return { phase: "idle" };
3556
+ const { controller: _c, done: _d, ...view } = current;
3557
+ return view;
3558
+ }
3559
+ function base64url(buf) {
3560
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
3561
+ }
3562
+ function createPkce() {
3563
+ const verifier = base64url(randomBytes2(32));
3564
+ const challenge = base64url(createHash3("sha256").update(verifier).digest());
3565
+ return { verifier, challenge };
3566
+ }
3567
+ function verifyState(received, expected) {
3568
+ const a = Buffer.from(received);
3569
+ const b = Buffer.from(expected);
3570
+ return a.length === b.length && timingSafeEqual2(a, b);
3571
+ }
3572
+ function buildAuthorizeUrl(params) {
3573
+ const url = new URL(params.authorizeEndpoint);
3574
+ url.searchParams.set("client_id", params.clientId);
3575
+ url.searchParams.set("redirect_uri", params.redirectUri);
3576
+ url.searchParams.set("scope", params.scope);
3577
+ url.searchParams.set("state", params.state);
3578
+ url.searchParams.set("code_challenge", params.challenge);
3579
+ url.searchParams.set("code_challenge_method", "S256");
3580
+ return url.toString();
3581
+ }
3582
+ async function exchangeCode(fetchImpl, endpoint, params, signal) {
3583
+ const response = await fetchImpl(endpoint, {
3584
+ method: "POST",
3585
+ headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
3586
+ body: new URLSearchParams({
3587
+ client_id: params.clientId,
3588
+ code: params.code,
3589
+ code_verifier: params.verifier,
3590
+ redirect_uri: params.redirectUri
3591
+ }).toString(),
3592
+ signal
3593
+ });
3594
+ if (!response.ok) throw new Error(`GitHub access_token HTTP ${response.status}`);
3595
+ const body = await response.json();
3596
+ if (typeof body.access_token !== "string" || body.access_token === "") {
3597
+ throw new Error(`GitHub access_token \u672A\u8FD4\u56DE token${typeof body.error === "string" ? `\uFF08${body.error}\uFF09` : ""}`);
3598
+ }
3599
+ return {
3600
+ token: body.access_token,
3601
+ ...typeof body.token_type === "string" ? { tokenType: body.token_type } : {},
3602
+ ...typeof body.scope === "string" ? { scope: body.scope } : {}
3603
+ };
3604
+ }
3605
+ async function fetchLogin(fetchImpl, endpoint, token, signal) {
3606
+ try {
3607
+ const response = await fetchImpl(endpoint, {
3608
+ headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json", "user-agent": "dsh-github-trending" },
3609
+ signal
3610
+ });
3611
+ if (!response.ok) return void 0;
3612
+ const body = await response.json();
3613
+ return typeof body.login === "string" ? body.login : void 0;
3614
+ } catch {
3615
+ return void 0;
3616
+ }
3617
+ }
3618
+ async function runBrowser(deps, session) {
3619
+ const webServer = deps.get("webServer");
3620
+ if (webServer === void 0 || typeof webServer.register !== "function") {
3621
+ throw new Error("webServer \u670D\u52A1\u4E0D\u53EF\u7528\uFF1A\u6D4F\u89C8\u5668\u767B\u5F55\u9700\u8981\u5B83\u627F\u8F7D OAuth \u56DE\u8C03");
3622
+ }
3623
+ const clientId = deps.clientId;
3624
+ if (clientId === void 0 || clientId === "") throw new Error("\u7F3A\u5C11 GitHub OAuth client_id");
3625
+ const fetchImpl = deps.fetchImpl ?? fetch;
3626
+ const endpoints = normalizeEndpoints(deps.endpoints);
3627
+ const scope = deps.scope ?? DEFAULT_SCOPE;
3628
+ const state = base64url(randomBytes2(32));
3629
+ const { verifier, challenge } = createPkce();
3630
+ let resolveCode;
3631
+ let rejectCode;
3632
+ const codePromise = new Promise((resolve, reject) => {
3633
+ resolveCode = resolve;
3634
+ rejectCode = reject;
3635
+ });
3636
+ codePromise.catch(() => void 0);
3637
+ const dispose = webServer.register({
3638
+ kind: "exact",
3639
+ path: CALLBACK_PATH,
3640
+ handler: (req, res) => {
3641
+ let url;
3642
+ try {
3643
+ url = new URL(req.url ?? "/", "http://127.0.0.1");
3644
+ } catch {
3645
+ res.writeHead(400, { "cache-control": "no-store" }).end();
3646
+ return;
3647
+ }
3648
+ const code = url.searchParams.get("code") ?? "";
3649
+ const gotState = url.searchParams.get("state") ?? "";
3650
+ const single = url.searchParams.getAll("state").length === 1 && url.searchParams.getAll("code").length === 1;
3651
+ const bad = req.method !== "GET" || url.pathname !== CALLBACK_PATH || !single || code === "" || !verifyState(gotState, state);
3652
+ if (bad) {
3653
+ res.writeHead(400, { "cache-control": "no-store" }).end();
3654
+ return;
3655
+ }
3656
+ 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>');
3657
+ resolveCode(code);
3658
+ }
3659
+ });
3660
+ try {
3661
+ const origin = localOrigin(webServer);
3662
+ const redirectUri = `${origin}${CALLBACK_PATH}`;
3663
+ const authorizeUrl = buildAuthorizeUrl({
3664
+ authorizeEndpoint: endpoints.authorize,
3665
+ clientId,
3666
+ redirectUri,
3667
+ scope,
3668
+ state,
3669
+ challenge
3670
+ });
3671
+ updateCurrent({ phase: "waiting-browser", authorizeUrl, expiresAt: Date.now() + attemptTimeout(deps) });
3672
+ session.notify?.({ message: "Continue in your browser to sign in to GitHub.", url: authorizeUrl });
3673
+ const code = await withAbort(codePromise, session.signal, () => rejectCode(new Error("authorization cancelled")));
3674
+ updateCurrent({ phase: "exchanging" });
3675
+ const exchanged = await exchangeCode(fetchImpl, endpoints.accessToken, { clientId, code, verifier, redirectUri }, session.signal);
3676
+ const login = await fetchLogin(fetchImpl, endpoints.user, exchanged.token, session.signal);
3677
+ const payload = {
3678
+ version: 1,
3679
+ token: exchanged.token,
3680
+ ...login === void 0 ? {} : { login },
3681
+ tokenType: exchanged.tokenType ?? "bearer",
3682
+ scope: exchanged.scope ?? scope
3683
+ };
3684
+ if (typeof session.commit === "function") await session.commit({ kind: "grant", payload });
3685
+ updateCurrent({ phase: "done", ...login === void 0 ? {} : { login } });
3686
+ } finally {
3687
+ dispose();
3688
+ }
3689
+ }
3690
+ async function runDevice(deps, session) {
3691
+ const clientId = deps.clientId;
3692
+ if (clientId === void 0 || clientId === "") throw new Error("\u7F3A\u5C11 GitHub OAuth client_id");
3693
+ const fetchImpl = deps.fetchImpl ?? fetch;
3694
+ const endpoints = normalizeEndpoints(deps.endpoints);
3695
+ const scope = deps.scope ?? DEFAULT_SCOPE;
3696
+ const startResponse = await fetchImpl(endpoints.deviceCode, {
3697
+ method: "POST",
3698
+ headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
3699
+ body: new URLSearchParams({ client_id: clientId, scope }).toString(),
3700
+ signal: session.signal
3701
+ });
3702
+ if (!startResponse.ok) throw new Error(`GitHub device/code HTTP ${startResponse.status}`);
3703
+ const start = await startResponse.json();
3704
+ if (typeof start.device_code !== "string" || typeof start.user_code !== "string" || typeof start.verification_uri !== "string") {
3705
+ throw new Error("GitHub device/code \u8FD4\u56DE\u4E0D\u5B8C\u6574");
3706
+ }
3707
+ const deviceCode = start.device_code;
3708
+ const intervalSec = typeof start.interval === "number" && start.interval > 0 ? start.interval : 5;
3709
+ const expiresInSec = typeof start.expires_in === "number" && start.expires_in > 0 ? start.expires_in : 900;
3710
+ const deadline = Date.now() + expiresInSec * 1e3;
3711
+ updateCurrent({
3712
+ phase: "waiting-device",
3713
+ verificationUri: start.verification_uri,
3714
+ userCode: start.user_code,
3715
+ expiresAt: deadline
3716
+ });
3717
+ session.notify?.({
3718
+ message: "Enter this code on the verification page to finish signing in.",
3719
+ url: start.verification_uri,
3720
+ code: start.user_code
3721
+ });
3722
+ let waitMs = intervalSec * 1e3;
3723
+ while (Date.now() < deadline) {
3724
+ await sleep(waitMs, session.signal);
3725
+ const pollResponse = await fetchImpl(endpoints.accessToken, {
3726
+ method: "POST",
3727
+ headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
3728
+ body: new URLSearchParams({
3729
+ client_id: clientId,
3730
+ device_code: deviceCode,
3731
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
3732
+ }).toString(),
3733
+ signal: session.signal
3734
+ });
3735
+ if (!pollResponse.ok) throw new Error(`GitHub access_token HTTP ${pollResponse.status}`);
3736
+ const poll = await pollResponse.json();
3737
+ if (typeof poll.access_token === "string" && poll.access_token !== "") {
3738
+ updateCurrent({ phase: "exchanging" });
3739
+ const login = await fetchLogin(fetchImpl, endpoints.user, poll.access_token, session.signal);
3740
+ const payload = {
3741
+ version: 1,
3742
+ token: poll.access_token,
3743
+ ...login === void 0 ? {} : { login },
3744
+ tokenType: typeof poll.token_type === "string" ? poll.token_type : "bearer",
3745
+ scope: typeof poll.scope === "string" ? poll.scope : scope
3746
+ };
3747
+ if (typeof session.commit === "function") await session.commit({ kind: "grant", payload });
3748
+ updateCurrent({ phase: "done", ...login === void 0 ? {} : { login } });
3749
+ return;
3750
+ }
3751
+ if (poll.error === "authorization_pending") continue;
3752
+ if (poll.error === "slow_down") {
3753
+ waitMs += (typeof poll.interval === "number" && poll.interval > 0 ? poll.interval : 5) * 1e3;
3754
+ continue;
3755
+ }
3756
+ throw new Error(`GitHub device flow \u7EC8\u6B62\uFF1A${typeof poll.error === "string" ? poll.error : "unknown"}`);
3757
+ }
3758
+ throw new Error("GitHub device flow \u8D85\u65F6");
3759
+ }
3760
+ function registerGithubFlow(deps) {
3761
+ const authorization = deps.get("authorization");
3762
+ if (authorization === void 0 || typeof authorization.registerFlow !== "function") {
3763
+ deps.logger?.("auth.flow.skipped", { reason: "no-authorization-service" });
3764
+ return;
3765
+ }
3766
+ authorization.registerFlow({
3767
+ key: GITHUB_RECORD_KEY2,
3768
+ label: "GitHub",
3769
+ methods: [
3770
+ { id: "browser", label: "Sign in with GitHub" },
3771
+ { id: "device", label: "Use a device code" }
3772
+ ],
3773
+ run: (session) => session.method === "device" ? runDevice(deps, session) : runBrowser(deps, session)
3774
+ });
3775
+ deps.logger?.("auth.flow.registered", { key: GITHUB_RECORD_KEY2 });
3776
+ }
3777
+ async function beginGithubSignIn(deps, method = "browser") {
3778
+ const authorization = deps.get("authorization");
3779
+ if (authorization === void 0 || typeof authorization.begin !== "function") {
3780
+ throw new Error("authorization \u670D\u52A1\u4E0D\u53EF\u7528\uFF1A\u65E0\u6CD5\u8FDB\u884C\u6D4F\u89C8\u5668\u767B\u5F55");
3781
+ }
3782
+ if (current !== void 0 && (current.phase === "waiting-browser" || current.phase === "waiting-device" || current.phase === "exchanging")) {
3783
+ throw new Error("already-in-flight");
3784
+ }
3785
+ const controller = new AbortController();
3786
+ const timer = setTimeout(() => controller.abort(), attemptTimeout(deps));
3787
+ const attempt = { phase: "idle", controller, done: Promise.resolve() };
3788
+ current = attempt;
3789
+ attempt.done = authorization.begin({
3790
+ key: GITHUB_RECORD_KEY2,
3791
+ method,
3792
+ signal: controller.signal,
3793
+ // 浏览器往返靠 authorizeUrl + 回调;notify 只更新内存视图,prompt 不支持(拒绝)。
3794
+ interaction: {
3795
+ notify: (notice) => {
3796
+ if (notice.code !== void 0 && notice.url !== void 0) {
3797
+ updateCurrent({ phase: "waiting-device", verificationUri: notice.url, userCode: notice.code });
3798
+ } else if (notice.url !== void 0) {
3799
+ updateCurrent({ phase: "waiting-browser", authorizeUrl: notice.url });
3800
+ }
3801
+ },
3802
+ prompt: () => Promise.reject(new Error("interactive prompt unsupported in this surface"))
3803
+ }
3804
+ }).then((outcome) => {
3805
+ if (outcome.status === "cancelled") updateCurrent({ phase: "idle" });
3806
+ }).catch((error) => {
3807
+ updateCurrent({ phase: "error", error: error instanceof Error ? error.message : String(error) });
3808
+ }).finally(() => clearTimeout(timer));
3809
+ await Promise.resolve();
3810
+ return attemptView();
3811
+ }
3812
+ function cancelGithubSignIn(deps) {
3813
+ const authorization = deps.get("authorization");
3814
+ if (authorization !== void 0 && typeof authorization.cancel === "function") {
3815
+ authorization.cancel(GITHUB_RECORD_KEY2);
3816
+ }
3817
+ if (current !== void 0) current.controller.abort();
3818
+ }
3819
+ function resetAttemptForTest() {
3820
+ current = void 0;
3821
+ }
3822
+ function normalizeEndpoints(endpoints = {}) {
3823
+ return {
3824
+ authorize: endpoints?.authorize ?? AUTHORIZE_URL,
3825
+ accessToken: endpoints?.accessToken ?? ACCESS_TOKEN_URL,
3826
+ deviceCode: endpoints?.deviceCode ?? DEVICE_CODE_URL,
3827
+ user: endpoints?.user ?? GITHUB_USER_URL
3828
+ };
3829
+ }
3830
+ function attemptTimeout(deps) {
3831
+ return typeof deps.attemptTimeoutMs === "number" && deps.attemptTimeoutMs > 0 ? deps.attemptTimeoutMs : DEFAULT_ATTEMPT_TIMEOUT_MS;
3832
+ }
3833
+ function localOrigin(webServer) {
3834
+ const port = webServer.port;
3835
+ if (typeof port !== "number" || port <= 0) throw new Error("webServer \u5C1A\u672A\u76D1\u542C\uFF0C\u65E0\u6CD5\u786E\u5B9A\u56DE\u8C03\u5730\u5740");
3836
+ const host = typeof webServer.host === "string" && webServer.host !== "" ? webServer.host : "127.0.0.1";
3837
+ const loopback = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
3838
+ return `http://${loopback}:${port}`;
3839
+ }
3840
+ function withAbort(promise, signal, onAbort) {
3841
+ if (signal === void 0) return promise;
3842
+ if (signal.aborted) {
3843
+ onAbort();
3844
+ return Promise.reject(new Error("authorization cancelled"));
3845
+ }
3846
+ return new Promise((resolve, reject) => {
3847
+ const abort = () => {
3848
+ onAbort();
3849
+ reject(new Error("authorization cancelled"));
3850
+ };
3851
+ signal.addEventListener("abort", abort, { once: true });
3852
+ promise.then(
3853
+ (value) => {
3854
+ signal.removeEventListener("abort", abort);
3855
+ resolve(value);
3856
+ },
3857
+ (error) => {
3858
+ signal.removeEventListener("abort", abort);
3859
+ reject(error);
3860
+ }
3861
+ );
3862
+ });
3863
+ }
3864
+ function sleep(ms, signal) {
3865
+ return new Promise((resolve, reject) => {
3866
+ if (signal?.aborted === true) {
3867
+ reject(new Error("authorization cancelled"));
3868
+ return;
3869
+ }
3870
+ const timer = setTimeout(() => {
3871
+ signal?.removeEventListener("abort", abort);
3872
+ resolve();
3873
+ }, ms);
3874
+ const abort = () => {
3875
+ clearTimeout(timer);
3876
+ reject(new Error("authorization cancelled"));
3877
+ };
3878
+ signal?.addEventListener("abort", abort, { once: true });
3879
+ });
3880
+ }
3881
+ function updateCurrent(patch) {
3882
+ if (current === void 0) return;
3883
+ Object.assign(current, patch);
3884
+ }
3885
+
3488
3886
  // src/host/import-cli.ts
3489
3887
  var import_cli_exports = {};
3490
3888
  __export(import_cli_exports, {
@@ -3532,7 +3930,7 @@ function scalar(raw) {
3532
3930
  }
3533
3931
  function parseSimpleYaml(text) {
3534
3932
  const root = {};
3535
- let current;
3933
+ let current2;
3536
3934
  let currentIndent = -1;
3537
3935
  for (const rawLine of text.split("\n")) {
3538
3936
  const line = rawLine.replace(/\r$/, "");
@@ -3546,21 +3944,21 @@ function parseSimpleYaml(text) {
3546
3944
  if (rest.trim() === "") {
3547
3945
  const child = {};
3548
3946
  root[key] = child;
3549
- current = child;
3947
+ current2 = child;
3550
3948
  currentIndent = indent;
3551
3949
  continue;
3552
3950
  }
3553
3951
  if (indent === 0) {
3554
3952
  root[key] = scalar(rest);
3555
- current = void 0;
3953
+ current2 = void 0;
3556
3954
  currentIndent = -1;
3557
3955
  continue;
3558
3956
  }
3559
- if (current === void 0 || indent <= currentIndent) {
3957
+ if (current2 === void 0 || indent <= currentIndent) {
3560
3958
  root[key] = scalar(rest);
3561
3959
  continue;
3562
3960
  }
3563
- current[key] = scalar(rest);
3961
+ current2[key] = scalar(rest);
3564
3962
  }
3565
3963
  return root;
3566
3964
  }
@@ -3983,15 +4381,23 @@ import { PluginUpdateError } from "@morehao/dsh-plugin-kit/update";
3983
4381
  var ROUTE_PREFIX = "/api/morehao/github-trending";
3984
4382
  var HEADER = "x-dsh-github-trending";
3985
4383
  var MAX_BODY2 = 512 * 1024;
4384
+ function githubAuthDeps(deps) {
4385
+ return {
4386
+ get: deps.get,
4387
+ ...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl },
4388
+ ...deps.githubClientId === void 0 ? {} : { clientId: deps.githubClientId },
4389
+ ...deps.logger === void 0 ? {} : { logger: deps.logger }
4390
+ };
4391
+ }
3986
4392
  function createRoutes(deps) {
3987
4393
  const logger = deps.logger;
3988
- let current = { method: "GET", pathname: "/" };
4394
+ let current2 = { method: "GET", pathname: "/" };
3989
4395
  const handler = createRouteHandler({
3990
4396
  prefix: ROUTE_PREFIX,
3991
4397
  header: HEADER,
3992
4398
  maxBodyBytes: MAX_BODY2,
3993
4399
  dispatch: async ({ method, pathname, url, body }) => {
3994
- current = { method, pathname };
4400
+ current2 = { method, pathname };
3995
4401
  return dispatch(method, pathname, url, body, deps);
3996
4402
  },
3997
4403
  // 错误码翻译留在插件侧(dispatcher 不做映射):保持与原实现的响应形状逐字一致。
@@ -4011,7 +4417,7 @@ function createRoutes(deps) {
4011
4417
  }
4012
4418
  const code = error instanceof IpcError ? error.code : "INTERNAL";
4013
4419
  const message = redact(error instanceof Error ? error.message : String(error));
4014
- logger?.("route.error", { pathname: current.pathname, method: current.method, code, message });
4420
+ logger?.("route.error", { pathname: current2.pathname, method: current2.method, code, message });
4015
4421
  const status = code === "CONFIG_INVALID" ? (
4016
4422
  // 「已有一次运行在进行中」是并发冲突而非参数错误:用 409,前端据此禁用按钮并提示。
4017
4423
  message.includes("\u8FD0\u884C\u5728\u8FDB\u884C\u4E2D") ? 409 : 400
@@ -4165,6 +4571,26 @@ async function dispatch(method, pathname, url, body, deps) {
4165
4571
  });
4166
4572
  return { github: probe };
4167
4573
  }
4574
+ if (method === "POST" && pathname === "/github/browser/begin") {
4575
+ const method2 = body.method === "device" ? "device" : "browser";
4576
+ try {
4577
+ const view = await beginGithubSignIn(githubAuthDeps(deps), method2);
4578
+ return { attempt: view };
4579
+ } catch (error) {
4580
+ const message = error instanceof Error ? error.message : String(error);
4581
+ if (message === "already-in-flight") {
4582
+ return { attempt: attemptView(), alreadyInFlight: true };
4583
+ }
4584
+ throw new IpcError("GITHUB_AUTH_UNAVAILABLE", redact(message));
4585
+ }
4586
+ }
4587
+ if (method === "GET" && pathname === "/github/browser/status") {
4588
+ return { attempt: attemptView() };
4589
+ }
4590
+ if (method === "POST" && pathname === "/github/browser/cancel") {
4591
+ cancelGithubSignIn(githubAuthDeps(deps));
4592
+ return { attempt: attemptView() };
4593
+ }
4168
4594
  if (method === "GET" && pathname === "/quickfill") {
4169
4595
  const candidates = await listProviderCandidates({
4170
4596
  llm: llmServices(deps.get),
@@ -4293,7 +4719,11 @@ async function statePayload(deps, settings) {
4293
4719
  github: {
4294
4720
  authMode: settings.github.authMode,
4295
4721
  configured: github.token !== void 0,
4296
- source: github.source
4722
+ source: github.source,
4723
+ /** 浏览器登录 attempt 视图(idle / waiting-browser / waiting-device / exchanging / done / error)。 */
4724
+ attempt: attemptView(),
4725
+ /** 是否配置了 browser 模式的 client_id(未配置时前端禁用「用 GitHub 登录」并给提示)。 */
4726
+ browserAvailable: deps.githubClientId !== void 0 && deps.githubClientId !== ""
4297
4727
  }
4298
4728
  };
4299
4729
  }
@@ -4572,6 +5002,8 @@ export {
4572
5002
  readPluginInfo,
4573
5003
  plugin_info_exports,
4574
5004
  github_exports,
5005
+ registerGithubFlow,
5006
+ github_auth_exports,
4575
5007
  import_cli_exports,
4576
5008
  pending_pr_exports,
4577
5009
  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-6PVBPJXO.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 = () => {