ghc-proxy 0.5.4 → 0.5.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/README.md CHANGED
@@ -118,6 +118,7 @@ bunx ghc-proxy@latest debug # Print diagnostic info (version, paths, to
118
118
  | `--proxy-env` | -- | `false` | Use `HTTP_PROXY`/`HTTPS_PROXY` from env (Node.js only; Bun reads proxy env natively) |
119
119
  | `--idle-timeout` | -- | `120` | Bun server idle timeout in seconds (`0` disables; Bun max is `255`; streaming routes disable idle timeout automatically) |
120
120
  | `--upstream-timeout` | -- | `1800` | Upstream request timeout in seconds (0 to disable) |
121
+ | `--ghe-domain` | `--ghe` | -- | GitHub Enterprise Cloud company domain (e.g. `company.ghe.com`). Required for GHE.com device login on first run; persisted automatically for later runs. |
121
122
 
122
123
  ## Rate Limiting
123
124
 
@@ -147,6 +148,28 @@ bunx ghc-proxy@latest start --account-type enterprise
147
148
 
148
149
  This routes requests to the correct Copilot API endpoint for your plan. See the [GitHub docs on network routing](https://docs.github.com/en/enterprise-cloud@latest/copilot/managing-copilot/managing-github-copilot-in-your-organization/managing-access-to-github-copilot-in-your-organization/managing-github-copilot-access-to-your-organizations-network#configuring-copilot-subscription-based-network-routing-for-your-enterprise-or-organization) for details.
149
150
 
151
+ ### GitHub Enterprise Cloud (GHE.com)
152
+
153
+ If your organization uses GitHub Enterprise Cloud (`*.ghe.com`), the standard GitHub device login URL differs from `github.com`. Pass your company's GHE domain on first auth:
154
+
155
+ ```bash
156
+ bunx ghc-proxy@latest start --account-type enterprise --ghe-domain company.ghe.com
157
+ ```
158
+
159
+ Or authenticate first, then start without the flag on subsequent runs:
160
+
161
+ ```bash
162
+ # First run (authenticates and persists the domain)
163
+ bunx ghc-proxy@latest auth --ghe-domain company.ghe.com
164
+
165
+ # Later runs (domain is read from persisted config)
166
+ bunx ghc-proxy@latest start --account-type enterprise
167
+ ```
168
+
169
+ The proxy normalizes and persists the GHE domain automatically after a successful authentication, so you only need to pass `--ghe-domain` on the first run or when switching tenants.
170
+
171
+ > **Note:** `--account-type enterprise` alone is not sufficient for GHE.com login — the proxy needs the company domain to construct the correct device login URL (`https://<company>.ghe.com/login/device`). GHE.com support is scoped to `*.ghe.com` only and does not apply to self-hosted GitHub Enterprise Server instances.
172
+
150
173
  ## Configuration
151
174
 
152
175
  The proxy reads an optional JSON config file at:
package/dist/main.mjs CHANGED
@@ -5389,7 +5389,8 @@ const configFileSchema = object({
5389
5389
  to: string()
5390
5390
  })).optional(),
5391
5391
  contextUpgrade: boolean().optional(),
5392
- contextUpgradeTokenThreshold: number().int().positive().optional()
5392
+ contextUpgradeTokenThreshold: number().int().positive().optional(),
5393
+ gheDomain: string().optional()
5393
5394
  }).passthrough();
5394
5395
  const KNOWN_CONFIG_KEYS = new Set(Object.keys(configFileSchema.shape));
5395
5396
  let cachedConfig = {};
@@ -5499,6 +5500,105 @@ async function applyConfigFilePermissions(filePath) {
5499
5500
  }
5500
5501
  }
5501
5502
 
5503
+ //#endregion
5504
+ //#region src/lib/api-config.ts
5505
+ function standardHeaders() {
5506
+ return {
5507
+ "content-type": "application/json",
5508
+ "accept": "application/json"
5509
+ };
5510
+ }
5511
+ const COPILOT_VERSION = "0.26.7";
5512
+ const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
5513
+ const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
5514
+ const API_VERSION = "2025-04-01";
5515
+ const TRAILING_SLASHES_RE$1 = /\/+$/;
5516
+ /** Headers shared by both Copilot and GitHub API requests (editor identity + versioning) */
5517
+ function editorHeaders(config) {
5518
+ return {
5519
+ "editor-version": `vscode/${config.vsCodeVersion ?? "unknown"}`,
5520
+ "editor-plugin-version": EDITOR_PLUGIN_VERSION,
5521
+ "user-agent": USER_AGENT,
5522
+ "x-github-api-version": API_VERSION,
5523
+ "x-vscode-user-agent-library-version": "electron-fetch"
5524
+ };
5525
+ }
5526
+ function copilotBaseUrl(config) {
5527
+ if (config.copilotApiBase) return config.copilotApiBase.replace(TRAILING_SLASHES_RE$1, "");
5528
+ return config.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${config.accountType}.githubcopilot.com`;
5529
+ }
5530
+ function copilotHeaders(auth, config, options = {}) {
5531
+ const requestContext = options.requestContext;
5532
+ const headers = {
5533
+ "Authorization": `Bearer ${auth.copilotToken}`,
5534
+ "content-type": standardHeaders()["content-type"],
5535
+ "copilot-integration-id": "vscode-chat",
5536
+ ...editorHeaders(config),
5537
+ "openai-intent": "conversation-panel",
5538
+ "x-request-id": randomUUID()
5539
+ };
5540
+ if (options.vision) headers["copilot-vision-request"] = "true";
5541
+ if (options.initiator) headers["X-Initiator"] = options.initiator;
5542
+ if (requestContext?.interactionType) headers["X-Interaction-Type"] = requestContext.interactionType;
5543
+ if (requestContext?.agentTaskId) headers["X-Agent-Task-Id"] = requestContext.agentTaskId;
5544
+ if (requestContext?.parentAgentTaskId) headers["X-Parent-Agent-Id"] = requestContext.parentAgentTaskId;
5545
+ if (requestContext?.clientSessionId) headers["X-Client-Session-Id"] = requestContext.clientSessionId;
5546
+ if (requestContext?.interactionId) headers["X-Interaction-Id"] = requestContext.interactionId;
5547
+ if (requestContext?.clientMachineId) headers["X-Client-Machine-Id"] = requestContext.clientMachineId;
5548
+ return headers;
5549
+ }
5550
+ const GITHUB_API_BASE_URL = "https://api.github.com";
5551
+ function githubHeaders(auth, config) {
5552
+ return {
5553
+ ...standardHeaders(),
5554
+ authorization: `token ${auth.githubToken}`,
5555
+ ...editorHeaders(config)
5556
+ };
5557
+ }
5558
+ const GITHUB_BASE_URL = "https://github.com";
5559
+ const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
5560
+ const GITHUB_APP_SCOPES = ["read:user"].join(" ");
5561
+
5562
+ //#endregion
5563
+ //#region src/lib/ghe-domain.ts
5564
+ const GHE_SUFFIX = ".ghe.com";
5565
+ /**
5566
+ * Normalize a GHE domain input to a lowercase bare domain.
5567
+ *
5568
+ * Accepted inputs: `company.ghe.com`, `https://company.ghe.com`,
5569
+ * `https://Company.GHE.com/`, etc.
5570
+ *
5571
+ * @returns Bare lowercase domain, e.g. `company.ghe.com`
5572
+ * @throws {Error} If the input is empty or does not end with `.ghe.com`
5573
+ */
5574
+ function normalizeGheDomain(input) {
5575
+ const trimmed = input.trim();
5576
+ if (!trimmed) throw new Error("GHE domain must not be empty");
5577
+ let domain;
5578
+ try {
5579
+ domain = (trimmed.includes("://") ? new URL(trimmed) : new URL(`https://${trimmed}`)).hostname.toLowerCase();
5580
+ } catch {
5581
+ throw new Error(`Invalid GHE domain: ${trimmed}`);
5582
+ }
5583
+ if (!domain.endsWith(GHE_SUFFIX) || domain === GHE_SUFFIX.slice(1)) throw new Error(`GHE domain must end with ${GHE_SUFFIX} (got "${domain}")`);
5584
+ return domain;
5585
+ }
5586
+ /**
5587
+ * Build GitHub base URL and API base URL for a given GHE domain,
5588
+ * or return the public GitHub defaults when no domain is provided.
5589
+ */
5590
+ function buildGitHubUrls(gheDomain) {
5591
+ if (!gheDomain) return {
5592
+ baseUrl: GITHUB_BASE_URL,
5593
+ apiBaseUrl: GITHUB_API_BASE_URL
5594
+ };
5595
+ const domain = normalizeGheDomain(gheDomain);
5596
+ return {
5597
+ baseUrl: `https://${domain}`,
5598
+ apiBaseUrl: `https://api.${domain}`
5599
+ };
5600
+ }
5601
+
5502
5602
  //#endregion
5503
5603
  //#region node_modules/fetch-event-stream/esm/deps/jsr.io/@std/streams/0.221.0/text_line_stream.js
5504
5604
  /**
@@ -5624,65 +5724,6 @@ async function* events(res, signal) {
5624
5724
  }
5625
5725
  }
5626
5726
 
5627
- //#endregion
5628
- //#region src/lib/api-config.ts
5629
- function standardHeaders() {
5630
- return {
5631
- "content-type": "application/json",
5632
- "accept": "application/json"
5633
- };
5634
- }
5635
- const COPILOT_VERSION = "0.26.7";
5636
- const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
5637
- const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
5638
- const API_VERSION = "2025-04-01";
5639
- const TRAILING_SLASHES_RE$1 = /\/+$/;
5640
- /** Headers shared by both Copilot and GitHub API requests (editor identity + versioning) */
5641
- function editorHeaders(config) {
5642
- return {
5643
- "editor-version": `vscode/${config.vsCodeVersion ?? "unknown"}`,
5644
- "editor-plugin-version": EDITOR_PLUGIN_VERSION,
5645
- "user-agent": USER_AGENT,
5646
- "x-github-api-version": API_VERSION,
5647
- "x-vscode-user-agent-library-version": "electron-fetch"
5648
- };
5649
- }
5650
- function copilotBaseUrl(config) {
5651
- if (config.copilotApiBase) return config.copilotApiBase.replace(TRAILING_SLASHES_RE$1, "");
5652
- return config.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${config.accountType}.githubcopilot.com`;
5653
- }
5654
- function copilotHeaders(auth, config, options = {}) {
5655
- const requestContext = options.requestContext;
5656
- const headers = {
5657
- "Authorization": `Bearer ${auth.copilotToken}`,
5658
- "content-type": standardHeaders()["content-type"],
5659
- "copilot-integration-id": "vscode-chat",
5660
- ...editorHeaders(config),
5661
- "openai-intent": "conversation-panel",
5662
- "x-request-id": randomUUID()
5663
- };
5664
- if (options.vision) headers["copilot-vision-request"] = "true";
5665
- if (options.initiator) headers["X-Initiator"] = options.initiator;
5666
- if (requestContext?.interactionType) headers["X-Interaction-Type"] = requestContext.interactionType;
5667
- if (requestContext?.agentTaskId) headers["X-Agent-Task-Id"] = requestContext.agentTaskId;
5668
- if (requestContext?.parentAgentTaskId) headers["X-Parent-Agent-Id"] = requestContext.parentAgentTaskId;
5669
- if (requestContext?.clientSessionId) headers["X-Client-Session-Id"] = requestContext.clientSessionId;
5670
- if (requestContext?.interactionId) headers["X-Interaction-Id"] = requestContext.interactionId;
5671
- if (requestContext?.clientMachineId) headers["X-Client-Machine-Id"] = requestContext.clientMachineId;
5672
- return headers;
5673
- }
5674
- const GITHUB_API_BASE_URL = "https://api.github.com";
5675
- function githubHeaders(auth, config) {
5676
- return {
5677
- ...standardHeaders(),
5678
- authorization: `token ${auth.githubToken}`,
5679
- ...editorHeaders(config)
5680
- };
5681
- }
5682
- const GITHUB_BASE_URL = "https://github.com";
5683
- const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
5684
- const GITHUB_APP_SCOPES = ["read:user"].join(" ");
5685
-
5686
5727
  //#endregion
5687
5728
  //#region src/lib/error.ts
5688
5729
  /**
@@ -5915,13 +5956,13 @@ var GitHubClient = class {
5915
5956
  return await response.json();
5916
5957
  }
5917
5958
  async getCopilotUsage() {
5918
- return this.requestJson(`${GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: githubHeaders(this.auth, this.config) }, "Failed to get Copilot usage");
5959
+ return this.requestJson(`${this.config.githubApiBaseUrl ?? GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: githubHeaders(this.auth, this.config) }, "Failed to get Copilot usage");
5919
5960
  }
5920
5961
  async getCopilotToken() {
5921
- return this.requestJson(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, { headers: githubHeaders(this.auth, this.config) }, "Failed to get Copilot token");
5962
+ return this.requestJson(`${this.config.githubApiBaseUrl ?? GITHUB_API_BASE_URL}/copilot_internal/v2/token`, { headers: githubHeaders(this.auth, this.config) }, "Failed to get Copilot token");
5922
5963
  }
5923
5964
  async getDeviceCode() {
5924
- return this.requestJson(`${GITHUB_BASE_URL}/login/device/code`, {
5965
+ return this.requestJson(`${this.config.githubBaseUrl ?? GITHUB_BASE_URL}/login/device/code`, {
5925
5966
  method: "POST",
5926
5967
  headers: standardHeaders(),
5927
5968
  body: JSON.stringify({
@@ -5935,7 +5976,7 @@ var GitHubClient = class {
5935
5976
  const sleepDuration = (deviceCode.interval + 1) * 1e3;
5936
5977
  consola.debug(`Polling access token with interval of ${sleepDuration}ms`);
5937
5978
  for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
5938
- const response = await this.fetchImpl(`${GITHUB_BASE_URL}/login/oauth/access_token`, {
5979
+ const response = await this.fetchImpl(`${this.config.githubBaseUrl ?? GITHUB_BASE_URL}/login/oauth/access_token`, {
5939
5980
  method: "POST",
5940
5981
  headers: standardHeaders(),
5941
5982
  body: JSON.stringify({
@@ -5957,7 +5998,7 @@ var GitHubClient = class {
5957
5998
  throw new Error("Device code authorization timed out");
5958
5999
  }
5959
6000
  async getGitHubUser() {
5960
- return this.requestJson(`${GITHUB_API_BASE_URL}/user`, { headers: {
6001
+ return this.requestJson(`${this.config.githubApiBaseUrl ?? GITHUB_API_BASE_URL}/user`, { headers: {
5961
6002
  authorization: `token ${this.auth.githubToken}`,
5962
6003
  ...standardHeaders()
5963
6004
  } }, "Failed to get GitHub user");
@@ -6277,10 +6318,13 @@ const state = {
6277
6318
  responsesEmulator: responsesEmulatorState
6278
6319
  };
6279
6320
  function getClientConfig() {
6321
+ const { baseUrl, apiBaseUrl } = buildGitHubUrls(state.auth.gheDomain);
6280
6322
  return {
6281
6323
  accountType: state.config.accountType,
6282
6324
  vsCodeVersion: state.cache.vsCodeVersion,
6283
- copilotApiBase: state.auth.copilotApiBase
6325
+ copilotApiBase: state.auth.copilotApiBase,
6326
+ githubBaseUrl: baseUrl,
6327
+ githubApiBaseUrl: apiBaseUrl
6284
6328
  };
6285
6329
  }
6286
6330
  function createCopilotClient() {
@@ -6329,6 +6373,11 @@ async function setupGitHubToken(options) {
6329
6373
  try {
6330
6374
  await ensureVSCodeVersion();
6331
6375
  const githubToken = getCachedConfig().githubToken?.trim() || "";
6376
+ if (githubToken && !options?.force && isDomainChanged()) {
6377
+ consola.warn("GHE domain changed — cached token is for a different GitHub instance. Re-authenticating...");
6378
+ await setupGitHubToken({ force: true });
6379
+ return;
6380
+ }
6332
6381
  if (githubToken && !options?.force) {
6333
6382
  state.auth.githubToken = githubToken;
6334
6383
  if (state.config.showToken) consola.info("GitHub token:", githubToken);
@@ -6352,6 +6401,7 @@ async function setupGitHubToken(options) {
6352
6401
  const token = await githubClient.pollAccessToken(response);
6353
6402
  await writeGithubToken(token);
6354
6403
  state.auth.githubToken = token;
6404
+ await writeConfigField("gheDomain", state.auth.gheDomain);
6355
6405
  if (state.config.showToken) consola.info("GitHub token:", token);
6356
6406
  await logUser();
6357
6407
  } catch (error) {
@@ -6382,6 +6432,13 @@ function normalizeCopilotApiBase(value) {
6382
6432
  if (!value) return;
6383
6433
  return value.replace(TRAILING_SLASHES_RE, "");
6384
6434
  }
6435
+ /**
6436
+ * Detects whether the runtime GHE domain differs from the previously persisted one.
6437
+ * Both `undefined` means "public github.com" → no change → returns false.
6438
+ */
6439
+ function isDomainChanged() {
6440
+ return state.auth.gheDomain !== getCachedConfig().gheDomain;
6441
+ }
6385
6442
  async function ensureVSCodeVersion() {
6386
6443
  if (!state.cache.vsCodeVersion) await cacheVSCodeVersion();
6387
6444
  }
@@ -6396,6 +6453,9 @@ async function runAuth(options) {
6396
6453
  state.config.showToken = options.showToken;
6397
6454
  await ensurePaths();
6398
6455
  await readConfig();
6456
+ state.auth.gheDomain = getCachedConfig().gheDomain;
6457
+ if (options.gheDomain !== void 0) state.auth.gheDomain = options.gheDomain ? normalizeGheDomain(options.gheDomain) : void 0;
6458
+ if (state.auth.gheDomain && state.config.accountType === "individual") state.config.accountType = "enterprise";
6399
6459
  await cacheVSCodeVersion();
6400
6460
  await setupGitHubToken({ force: true });
6401
6461
  consola.success("GitHub token written to config.json");
@@ -6416,12 +6476,18 @@ const auth = defineCommand({
6416
6476
  type: "boolean",
6417
6477
  default: false,
6418
6478
  description: "Show GitHub token on auth"
6479
+ },
6480
+ "ghe-domain": {
6481
+ alias: "ghe",
6482
+ type: "string",
6483
+ description: "Company GHE domain for GitHub Enterprise Cloud (e.g. company.ghe.com)"
6419
6484
  }
6420
6485
  },
6421
6486
  run({ args }) {
6422
6487
  return runAuth({
6423
6488
  verbose: args.verbose,
6424
- showToken: args["show-token"]
6489
+ showToken: args["show-token"],
6490
+ gheDomain: args["ghe-domain"]
6425
6491
  });
6426
6492
  }
6427
6493
  });
@@ -6436,6 +6502,7 @@ const checkUsage = defineCommand({
6436
6502
  async run() {
6437
6503
  await ensurePaths();
6438
6504
  await readConfig();
6505
+ state.auth.gheDomain = getCachedConfig().gheDomain;
6439
6506
  await cacheVSCodeVersion();
6440
6507
  await setupGitHubToken();
6441
6508
  try {
@@ -6466,7 +6533,7 @@ const checkUsage = defineCommand({
6466
6533
 
6467
6534
  //#endregion
6468
6535
  //#region src/lib/version.ts
6469
- const VERSION = "0.5.4";
6536
+ const VERSION = "0.5.6";
6470
6537
 
6471
6538
  //#endregion
6472
6539
  //#region src/debug.ts
@@ -6495,6 +6562,7 @@ async function checkConfigExists() {
6495
6562
  }
6496
6563
  }
6497
6564
  async function getDebugInfo() {
6565
+ await readConfig();
6498
6566
  const configExists = await checkConfigExists();
6499
6567
  return {
6500
6568
  version: VERSION,
@@ -6504,7 +6572,8 @@ async function getDebugInfo() {
6504
6572
  CONFIG_PATH: PATHS.CONFIG_PATH
6505
6573
  },
6506
6574
  configExists,
6507
- tokenExists: hasToken()
6575
+ tokenExists: hasToken(),
6576
+ gheDomain: getCachedConfig().gheDomain
6508
6577
  };
6509
6578
  }
6510
6579
  function printDebugInfoPlain(info) {
@@ -6518,7 +6587,8 @@ Paths:
6518
6587
  - CONFIG_PATH: ${info.paths.CONFIG_PATH}
6519
6588
 
6520
6589
  Config exists: ${info.configExists ? "Yes" : "No"}
6521
- Token exists: ${info.tokenExists ? "Yes" : "No"}`);
6590
+ Token exists: ${info.tokenExists ? "Yes" : "No"}
6591
+ GHE Domain: ${info.gheDomain ?? "none"}`);
6522
6592
  }
6523
6593
  async function runDebug(options) {
6524
6594
  const debugInfo = await getDebugInfo();
@@ -51701,6 +51771,9 @@ async function runServer(options) {
51701
51771
  state.config.upstreamTimeoutSeconds = options.upstreamTimeoutSeconds;
51702
51772
  await ensurePaths();
51703
51773
  await readConfig();
51774
+ state.auth.gheDomain = getCachedConfig().gheDomain;
51775
+ if (options.gheDomain !== void 0) state.auth.gheDomain = options.gheDomain ? normalizeGheDomain(options.gheDomain) : void 0;
51776
+ if (state.auth.gheDomain && state.config.accountType === "individual") state.config.accountType = "enterprise";
51704
51777
  await cacheVSCodeVersion();
51705
51778
  if (!options.githubToken) await setupGitHubToken();
51706
51779
  await setupCopilotToken();
@@ -51792,6 +51865,11 @@ const start = defineCommand({
51792
51865
  type: "string",
51793
51866
  default: "1800",
51794
51867
  description: "Upstream request timeout in seconds (0 to disable)"
51868
+ },
51869
+ "ghe-domain": {
51870
+ alias: "ghe",
51871
+ type: "string",
51872
+ description: "Company GHE domain for GitHub Enterprise Cloud (e.g. company.ghe.com)"
51795
51873
  }
51796
51874
  },
51797
51875
  run({ args }) {
@@ -51810,7 +51888,8 @@ const start = defineCommand({
51810
51888
  showToken: args["show-token"],
51811
51889
  proxyEnv: args["proxy-env"],
51812
51890
  idleTimeoutSeconds,
51813
- upstreamTimeoutSeconds
51891
+ upstreamTimeoutSeconds,
51892
+ gheDomain: args["ghe-domain"]
51814
51893
  });
51815
51894
  }
51816
51895
  });