@dianshuv/copilot-api 0.21.6 → 0.21.7

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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/dist/main.mjs +34 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -31,6 +31,19 @@ npm install -g @dianshuv/copilot-api
31
31
  copilot-api start
32
32
  ```
33
33
 
34
+ ### GitHub Enterprise Cloud with data residency
35
+
36
+ Set the tenant hostname for both login and server startup. Omit
37
+ `COPILOT_GH_HOST` to use `github.com`.
38
+
39
+ ```sh
40
+ COPILOT_GH_HOST=example.ghe.com npx @dianshuv/copilot-api login
41
+ COPILOT_GH_HOST=example.ghe.com npx @dianshuv/copilot-api start
42
+ ```
43
+
44
+ Only `github.com` and `*.ghe.com` hosts are accepted. OAuth uses the tenant web
45
+ host, GitHub REST calls use `api.<tenant>.ghe.com`, and the Copilot API endpoint
46
+ is discovered from the authenticated tenant.
34
47
 
35
48
  ## Development
36
49
 
package/dist/main.mjs CHANGED
@@ -135,6 +135,7 @@ function initProxyFromEnv() {
135
135
  //#endregion
136
136
  //#region src/lib/state.ts
137
137
  const state = {
138
+ githubHost: "github.com",
138
139
  accountType: "individual",
139
140
  manualApprove: false,
140
141
  showToken: false,
@@ -193,7 +194,26 @@ function copilotHeaders(state, visionOrOptions) {
193
194
  if (options.vision) headers["copilot-vision-request"] = "true";
194
195
  return headers;
195
196
  }
196
- const GITHUB_API_BASE_URL = "https://api.github.com";
197
+ const DEFAULT_GITHUB_HOST = "github.com";
198
+ const GHE_TENANT_HOST_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.ghe\.com$/;
199
+ function resolveGitHubHost(copilotGitHubHost) {
200
+ const configuredHost = copilotGitHubHost?.trim() || DEFAULT_GITHUB_HOST;
201
+ let hostUrl = configuredHost;
202
+ if (!configuredHost.includes("://")) hostUrl = `https://${configuredHost}`;
203
+ let parsed;
204
+ try {
205
+ parsed = new URL(hostUrl);
206
+ } catch {
207
+ throw new Error("GitHub host must be github.com or a .ghe.com tenant hostname.");
208
+ }
209
+ if (parsed.protocol !== "https:") throw new Error("GitHub host must use HTTPS.");
210
+ if (parsed.username || parsed.password || parsed.port || parsed.pathname !== "/" || parsed.search || parsed.hash) throw new Error("GitHub host must be a hostname without a path.");
211
+ const hostname = parsed.hostname.toLowerCase();
212
+ if (hostname !== DEFAULT_GITHUB_HOST && !GHE_TENANT_HOST_PATTERN.test(hostname)) throw new Error("GitHub host must be github.com or a .ghe.com tenant.");
213
+ return hostname;
214
+ }
215
+ const githubBaseUrl = (state) => `https://${state.githubHost}`;
216
+ const githubApiBaseUrl = (state) => state.githubHost === "github.com" ? "https://api.github.com" : `https://api.${state.githubHost}`;
197
217
  const githubHeaders = (state) => ({
198
218
  accept: standardHeaders().accept,
199
219
  authorization: `Bearer ${state.githubToken}`,
@@ -203,7 +223,6 @@ const githubOAuthHeaders = (state) => ({
203
223
  ...standardHeaders(),
204
224
  "user-agent": userAgent(state)
205
225
  });
206
- const GITHUB_BASE_URL = "https://github.com";
207
226
  const GITHUB_CLIENT_ID = "Ov23ctDVkRmgkPke0Mmm";
208
227
  const GITHUB_APP_SCOPES = [
209
228
  "read:user",
@@ -471,7 +490,7 @@ function forwardError(c, error) {
471
490
  //#endregion
472
491
  //#region src/services/github/get-device-code.ts
473
492
  async function getDeviceCode() {
474
- const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, {
493
+ const response = await fetch(`${githubBaseUrl(state)}/login/device/code`, {
475
494
  method: "POST",
476
495
  headers: githubOAuthHeaders(state),
477
496
  body: JSON.stringify({
@@ -486,7 +505,7 @@ async function getDeviceCode() {
486
505
  //#endregion
487
506
  //#region src/services/github/get-user.ts
488
507
  async function getGitHubUser() {
489
- const response = await fetch(`${GITHUB_API_BASE_URL}/user`, { headers: githubHeaders(state) });
508
+ const response = await fetch(`${githubApiBaseUrl(state)}/user`, { headers: githubHeaders(state) });
490
509
  if (!response.ok) throw await HTTPError.fromResponse("Failed to get GitHub user", response);
491
510
  return await response.json();
492
511
  }
@@ -663,7 +682,7 @@ async function getCopilotCliVersion() {
663
682
  //#endregion
664
683
  //#region src/services/github/get-copilot-usage.ts
665
684
  const getCopilotUsage = async () => {
666
- const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: githubHeaders(state) });
685
+ const response = await fetch(`${githubApiBaseUrl(state)}/copilot_internal/user`, { headers: githubHeaders(state) });
667
686
  if (!response.ok) throw await HTTPError.fromResponse("Failed to get Copilot usage", response);
668
687
  return await response.json();
669
688
  };
@@ -715,6 +734,11 @@ const initCopilotIdentity = async () => {
715
734
  * GitHub OAuth token at an arbitrary origin.
716
735
  */
717
736
  const CAPI_HOST_PATTERN = /^api(?:\.[a-z0-9-]+)?\.githubcopilot\.com$/;
737
+ function isTrustedCopilotApiHost(hostname) {
738
+ if (CAPI_HOST_PATTERN.test(hostname)) return true;
739
+ if (!state.githubHost.endsWith(".ghe.com")) return false;
740
+ return hostname === state.githubHost || hostname.endsWith(`.${state.githubHost}`);
741
+ }
718
742
  const bootstrapCopilotSession = async () => {
719
743
  const usage = await getCopilotUsage();
720
744
  if (usage === null || typeof usage !== "object") throw new Error("bootstrapCopilotSession: /copilot_internal/user body was not an object.");
@@ -730,7 +754,7 @@ const bootstrapCopilotSession = async () => {
730
754
  }
731
755
  if (parsed.protocol !== "https:") throw new Error(`bootstrapCopilotSession: endpoints.api must use https:// (got ${parsed.protocol}).`);
732
756
  if (parsed.port !== "") throw new Error(`bootstrapCopilotSession: endpoints.api must use the default https port (got :${parsed.port}).`);
733
- if (!CAPI_HOST_PATTERN.test(parsed.hostname)) throw new Error(`bootstrapCopilotSession: endpoints.api host ${parsed.hostname} is not an api[.tier].githubcopilot.com host.`);
757
+ if (!isTrustedCopilotApiHost(parsed.hostname)) throw new Error(`bootstrapCopilotSession: endpoints.api host ${parsed.hostname} is not a trusted Copilot API host for ${state.githubHost}.`);
734
758
  state.copilotApiEndpoint = parsed.origin;
735
759
  state.copilotToken = state.githubToken;
736
760
  };
@@ -742,7 +766,7 @@ async function pollAccessToken(deviceCode) {
742
766
  consola.debug(`Polling access token with interval of ${sleepDuration}ms`);
743
767
  const expiresAt = Date.now() + deviceCode.expires_in * 1e3;
744
768
  while (Date.now() < expiresAt) {
745
- const response = await fetch(`${GITHUB_BASE_URL}/login/oauth/access_token`, {
769
+ const response = await fetch(`${githubBaseUrl(state)}/login/oauth/access_token`, {
746
770
  method: "POST",
747
771
  headers: githubOAuthHeaders(state),
748
772
  body: JSON.stringify({
@@ -997,7 +1021,7 @@ async function runLogout() {
997
1021
  }
998
1022
  }
999
1023
  consola.warn("This only removes the local token file. The OAuth App authorization is still active on GitHub and the token remains valid until revoked.");
1000
- consola.info(`To fully revoke access, visit https://github.com/settings/connections/applications/${GITHUB_CLIENT_ID} and click "Revoke access".`);
1024
+ consola.info(`To fully revoke access, visit ${githubBaseUrl(state)}/settings/connections/applications/${GITHUB_CLIENT_ID} and click "Revoke access".`);
1001
1025
  }
1002
1026
  const logout = defineCommand({
1003
1027
  meta: {
@@ -1011,7 +1035,7 @@ const logout = defineCommand({
1011
1035
 
1012
1036
  //#endregion
1013
1037
  //#region package.json
1014
- var version = "0.21.6";
1038
+ var version = "0.21.7";
1015
1039
 
1016
1040
  //#endregion
1017
1041
  //#region src/lib/event-loop-lag.ts
@@ -9472,6 +9496,7 @@ const start = defineCommand({
9472
9496
  //#endregion
9473
9497
  //#region src/main.ts
9474
9498
  consola.options.formatOptions.date = false;
9499
+ state.githubHost = resolveGitHubHost(process.env.COPILOT_GH_HOST);
9475
9500
  await runMain(defineCommand({
9476
9501
  meta: {
9477
9502
  name: "copilot-api",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.21.6",
3
+ "version": "0.21.7",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",