@openhoo/hoopilot 0.7.2 → 0.7.3
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 +3 -0
- package/dist/{chunk-TEDEVCKM.js → chunk-7GSQVYYT.js} +61 -7
- package/dist/chunk-7GSQVYYT.js.map +1 -0
- package/dist/cli.js +192 -84
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +1 -1
- package/dist/index.cjs +207 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +207 -67
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-TEDEVCKM.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
asRecord,
|
|
4
|
+
envValue,
|
|
5
|
+
isTrustedTokenBaseUrl,
|
|
6
|
+
main,
|
|
7
|
+
trimTrailingSlash,
|
|
8
|
+
truncatedResponseText
|
|
9
|
+
} from "./chunk-7GSQVYYT.js";
|
|
5
10
|
|
|
6
11
|
// src/cli.ts
|
|
7
12
|
import { spawn } from "child_process";
|
|
@@ -17,10 +22,25 @@ var StoredCopilotAuthError = class extends Error {
|
|
|
17
22
|
}
|
|
18
23
|
};
|
|
19
24
|
function authStorePath(env = process.env) {
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
const explicit = envValue(env.HOOPILOT_AUTH_FILE);
|
|
26
|
+
if (explicit) {
|
|
27
|
+
return explicit;
|
|
22
28
|
}
|
|
23
|
-
const
|
|
29
|
+
const xdg = envValue(env.XDG_CONFIG_HOME);
|
|
30
|
+
if (xdg) {
|
|
31
|
+
return join(xdg, "hoopilot", "auth.json");
|
|
32
|
+
}
|
|
33
|
+
const appdata = envValue(env.APPDATA);
|
|
34
|
+
if (appdata) {
|
|
35
|
+
return join(appdata, "hoopilot", "auth.json");
|
|
36
|
+
}
|
|
37
|
+
const home = envValue(env.HOME);
|
|
38
|
+
if (!home) {
|
|
39
|
+
throw new StoredCopilotAuthError(
|
|
40
|
+
"Cannot resolve Hoopilot auth file path without HOOPILOT_AUTH_FILE, XDG_CONFIG_HOME, APPDATA, or HOME."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const base = join(home, ".config");
|
|
24
44
|
return join(base, "hoopilot", "auth.json");
|
|
25
45
|
}
|
|
26
46
|
function readStoredCopilotAuth(path = authStorePath()) {
|
|
@@ -77,30 +97,6 @@ function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
|
77
97
|
}
|
|
78
98
|
}
|
|
79
99
|
|
|
80
|
-
// src/util.ts
|
|
81
|
-
function trimTrailingSlash(value) {
|
|
82
|
-
return value.replace(/\/+$/, "");
|
|
83
|
-
}
|
|
84
|
-
function isHttpsOrLoopbackUrl(rawUrl) {
|
|
85
|
-
let url;
|
|
86
|
-
try {
|
|
87
|
-
url = new URL(rawUrl);
|
|
88
|
-
} catch {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
if (url.protocol === "https:") {
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1" || url.hostname === "[::1]");
|
|
95
|
-
}
|
|
96
|
-
async function truncatedResponseText(response, max = 500) {
|
|
97
|
-
const text = await response.text();
|
|
98
|
-
return text.slice(0, max);
|
|
99
|
-
}
|
|
100
|
-
function asRecord(value) {
|
|
101
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
100
|
// src/auth.ts
|
|
105
101
|
var DEFAULT_COPILOT_API_BASE_URL = "https://api.githubcopilot.com";
|
|
106
102
|
var REFRESH_SKEW_MS = 6e4;
|
|
@@ -114,11 +110,15 @@ var CopilotAuthError = class extends Error {
|
|
|
114
110
|
var CopilotAuth = class {
|
|
115
111
|
#authStorePath;
|
|
116
112
|
#copilotApiBaseUrl;
|
|
113
|
+
#hasCopilotApiBaseUrlOverride;
|
|
117
114
|
#cachedAccess;
|
|
118
115
|
constructor(options = {}) {
|
|
119
|
-
|
|
116
|
+
const envAuthStorePath = envValue(options.env?.HOOPILOT_AUTH_FILE);
|
|
117
|
+
const envCopilotApiBaseUrl = envValue(options.env?.COPILOT_API_BASE_URL);
|
|
118
|
+
this.#authStorePath = options.authStorePath ?? envAuthStorePath;
|
|
119
|
+
this.#hasCopilotApiBaseUrlOverride = Boolean(options.copilotApiBaseUrl ?? envCopilotApiBaseUrl);
|
|
120
120
|
this.#copilotApiBaseUrl = trimTrailingSlash(
|
|
121
|
-
options.copilotApiBaseUrl ??
|
|
121
|
+
options.copilotApiBaseUrl ?? envCopilotApiBaseUrl ?? DEFAULT_COPILOT_API_BASE_URL
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
124
|
async getAccess() {
|
|
@@ -136,7 +136,9 @@ var CopilotAuth = class {
|
|
|
136
136
|
}
|
|
137
137
|
if (stored) {
|
|
138
138
|
return this.#cacheAccess({
|
|
139
|
-
apiBaseUrl: trimTrailingSlash(
|
|
139
|
+
apiBaseUrl: trimTrailingSlash(
|
|
140
|
+
this.#hasCopilotApiBaseUrlOverride ? this.#copilotApiBaseUrl : stored.apiBaseUrl ?? this.#copilotApiBaseUrl
|
|
141
|
+
),
|
|
140
142
|
expiresAtMs: Date.now() + STORED_TOKEN_TTL_MS,
|
|
141
143
|
source: "github-copilot-oauth",
|
|
142
144
|
token: stored.token
|
|
@@ -154,6 +156,8 @@ var CopilotAuth = class {
|
|
|
154
156
|
|
|
155
157
|
// src/copilot.ts
|
|
156
158
|
var DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
|
|
159
|
+
var ALLOWED_COPILOT_API_HOSTS = ["api.githubcopilot.com"];
|
|
160
|
+
var ALLOWED_GITHUB_API_HOSTS = ["api.github.com"];
|
|
157
161
|
var COPILOT_USAGE_API_VERSION = "2025-04-01";
|
|
158
162
|
function applyCopilotHeaders(headers, token) {
|
|
159
163
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
@@ -177,13 +181,15 @@ function applyGithubApiHeaders(headers, token) {
|
|
|
177
181
|
}
|
|
178
182
|
var CopilotClient = class {
|
|
179
183
|
#auth;
|
|
184
|
+
#allowUnsafeUpstream;
|
|
180
185
|
#fetch;
|
|
181
186
|
#githubApiBaseUrl;
|
|
182
187
|
constructor(options = {}) {
|
|
183
188
|
this.#auth = new CopilotAuth(options);
|
|
189
|
+
this.#allowUnsafeUpstream = envValue(options.env?.HOOPILOT_ALLOW_UNSAFE_UPSTREAM) === "1";
|
|
184
190
|
this.#fetch = options.fetch ?? fetch;
|
|
185
191
|
this.#githubApiBaseUrl = trimTrailingSlash(
|
|
186
|
-
options.githubApiBaseUrl ?? options.env?.HOOPILOT_GITHUB_API_BASE_URL ?? DEFAULT_GITHUB_API_BASE_URL
|
|
192
|
+
options.githubApiBaseUrl ?? envValue(options.env?.HOOPILOT_GITHUB_API_BASE_URL) ?? DEFAULT_GITHUB_API_BASE_URL
|
|
187
193
|
);
|
|
188
194
|
}
|
|
189
195
|
/**
|
|
@@ -192,9 +198,13 @@ var CopilotClient = class {
|
|
|
192
198
|
* accepted directly here — no Copilot token exchange is required to read quota.
|
|
193
199
|
*/
|
|
194
200
|
async usage(signal) {
|
|
195
|
-
if (!
|
|
201
|
+
if (!isTrustedTokenBaseUrl(
|
|
202
|
+
this.#githubApiBaseUrl,
|
|
203
|
+
ALLOWED_GITHUB_API_HOSTS,
|
|
204
|
+
this.#allowUnsafeUpstream
|
|
205
|
+
)) {
|
|
196
206
|
throw new Error(
|
|
197
|
-
`Refusing to send the GitHub OAuth token to
|
|
207
|
+
`Refusing to send the GitHub OAuth token to an untrusted GitHub API host: ${this.#githubApiBaseUrl}`
|
|
198
208
|
);
|
|
199
209
|
}
|
|
200
210
|
const access = await this.#auth.getAccess();
|
|
@@ -236,9 +246,13 @@ var CopilotClient = class {
|
|
|
236
246
|
}
|
|
237
247
|
async fetchCopilot(path, init) {
|
|
238
248
|
const access = await this.#auth.getAccess();
|
|
239
|
-
if (!
|
|
249
|
+
if (!isTrustedTokenBaseUrl(
|
|
250
|
+
access.apiBaseUrl,
|
|
251
|
+
ALLOWED_COPILOT_API_HOSTS,
|
|
252
|
+
this.#allowUnsafeUpstream
|
|
253
|
+
)) {
|
|
240
254
|
throw new Error(
|
|
241
|
-
`Refusing to send the GitHub OAuth token to
|
|
255
|
+
`Refusing to send the GitHub OAuth token to an untrusted Copilot API host: ${access.apiBaseUrl}`
|
|
242
256
|
);
|
|
243
257
|
}
|
|
244
258
|
const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
|
|
@@ -324,9 +338,9 @@ async function githubCopilotDeviceLogin(options = {}) {
|
|
|
324
338
|
const fetcher = options.fetch ?? fetch;
|
|
325
339
|
const sleeper = options.sleep ?? sleep;
|
|
326
340
|
const domain = normalizeDomain(
|
|
327
|
-
options.domain ?? env.HOOPILOT_GITHUB_DOMAIN ?? DEFAULT_GITHUB_DOMAIN
|
|
341
|
+
options.domain ?? envValue(env.HOOPILOT_GITHUB_DOMAIN) ?? DEFAULT_GITHUB_DOMAIN
|
|
328
342
|
);
|
|
329
|
-
const clientId = options.clientId ?? env.HOOPILOT_GITHUB_CLIENT_ID ?? env.COPILOT_GITHUB_CLIENT_ID ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
|
|
343
|
+
const clientId = options.clientId ?? envValue(env.HOOPILOT_GITHUB_CLIENT_ID) ?? envValue(env.COPILOT_GITHUB_CLIENT_ID) ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
|
|
330
344
|
const device = await requestDeviceCode(fetcher, domain, clientId);
|
|
331
345
|
const verificationUrl = device.verification_uri;
|
|
332
346
|
const userCode = device.user_code;
|
|
@@ -484,8 +498,8 @@ var noopLogger = {
|
|
|
484
498
|
};
|
|
485
499
|
function createHoopilotLogger(options = {}) {
|
|
486
500
|
const env = options.env ?? process.env;
|
|
487
|
-
const level = parseLogLevel(options.level ?? env.HOOPILOT_LOG_LEVEL);
|
|
488
|
-
const format = parseLogFormat(options.format ?? env.HOOPILOT_LOG_FORMAT);
|
|
501
|
+
const level = parseLogLevel(options.level ?? envValue(env.HOOPILOT_LOG_LEVEL));
|
|
502
|
+
const format = parseLogFormat(options.format ?? envValue(env.HOOPILOT_LOG_FORMAT));
|
|
489
503
|
const pinoOptions = {
|
|
490
504
|
base: {
|
|
491
505
|
service: "hoopilot",
|
|
@@ -535,7 +549,7 @@ function parseLogLevel(value) {
|
|
|
535
549
|
}
|
|
536
550
|
function shouldCreateLogger(options) {
|
|
537
551
|
return Boolean(
|
|
538
|
-
options.logger || options.logFormat || options.logLevel || options.env?.HOOPILOT_LOG_FORMAT || options.env?.HOOPILOT_LOG_LEVEL
|
|
552
|
+
options.logger || options.logFormat || options.logLevel || envValue(options.env?.HOOPILOT_LOG_FORMAT) || envValue(options.env?.HOOPILOT_LOG_LEVEL)
|
|
539
553
|
);
|
|
540
554
|
}
|
|
541
555
|
function errorDetails(error) {
|
|
@@ -600,12 +614,15 @@ function completionStreamFromChatStream(chatStream) {
|
|
|
600
614
|
const encoder = new TextEncoder();
|
|
601
615
|
const decoder = new TextDecoder();
|
|
602
616
|
let buffer = "";
|
|
603
|
-
let
|
|
617
|
+
let sawTerminalEvent = false;
|
|
604
618
|
return new ReadableStream({
|
|
605
619
|
async start(controller) {
|
|
606
620
|
const enqueue = (data) => {
|
|
607
621
|
controller.enqueue(encoder.encode(encodeDataSse(data)));
|
|
608
622
|
};
|
|
623
|
+
const markTerminal = () => {
|
|
624
|
+
sawTerminalEvent = true;
|
|
625
|
+
};
|
|
609
626
|
const reader = chatStream.getReader();
|
|
610
627
|
try {
|
|
611
628
|
while (true) {
|
|
@@ -614,20 +631,17 @@ function completionStreamFromChatStream(chatStream) {
|
|
|
614
631
|
break;
|
|
615
632
|
}
|
|
616
633
|
buffer += decoder.decode(result.value, { stream: true });
|
|
617
|
-
const
|
|
618
|
-
buffer =
|
|
619
|
-
for (const
|
|
620
|
-
|
|
621
|
-
sawDone = true;
|
|
622
|
-
});
|
|
634
|
+
const blocks = buffer.split(/\r?\n\r?\n/);
|
|
635
|
+
buffer = blocks.pop() ?? "";
|
|
636
|
+
for (const block of blocks) {
|
|
637
|
+
processCompletionSseBlock(block, enqueue, markTerminal);
|
|
623
638
|
}
|
|
624
639
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
});
|
|
640
|
+
const tail = `${buffer}${decoder.decode()}`;
|
|
641
|
+
if (tail.trim()) {
|
|
642
|
+
processCompletionSseBlock(tail, enqueue, markTerminal);
|
|
629
643
|
}
|
|
630
|
-
if (!
|
|
644
|
+
if (!sawTerminalEvent) {
|
|
631
645
|
enqueue("[DONE]");
|
|
632
646
|
}
|
|
633
647
|
controller.close();
|
|
@@ -731,17 +745,23 @@ function firstChoice(completion) {
|
|
|
731
745
|
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
732
746
|
return asRecord(choices[0]);
|
|
733
747
|
}
|
|
734
|
-
function
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
748
|
+
function processCompletionSseBlock(block, enqueue, markTerminal) {
|
|
749
|
+
let event = "message";
|
|
750
|
+
const dataLines = [];
|
|
751
|
+
for (const line of block.split(/\r?\n/)) {
|
|
752
|
+
const trimmed = line.trim();
|
|
753
|
+
if (trimmed.startsWith("event:")) {
|
|
754
|
+
event = trimmed.slice("event:".length).trim() || event;
|
|
755
|
+
} else if (trimmed.startsWith("data:")) {
|
|
756
|
+
dataLines.push(trimmed.slice("data:".length).trim());
|
|
757
|
+
}
|
|
738
758
|
}
|
|
739
|
-
const data =
|
|
759
|
+
const data = dataLines.join("\n");
|
|
740
760
|
if (!data) {
|
|
741
761
|
return;
|
|
742
762
|
}
|
|
743
763
|
if (data === "[DONE]") {
|
|
744
|
-
|
|
764
|
+
markTerminal();
|
|
745
765
|
enqueue("[DONE]");
|
|
746
766
|
return;
|
|
747
767
|
}
|
|
@@ -749,6 +769,12 @@ function processCompletionSseLine(line, enqueue, markDone) {
|
|
|
749
769
|
if (!parsed) {
|
|
750
770
|
return;
|
|
751
771
|
}
|
|
772
|
+
const error = completionStreamError(event, parsed);
|
|
773
|
+
if (error) {
|
|
774
|
+
markTerminal();
|
|
775
|
+
enqueue({ error });
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
752
778
|
const choice = firstChoice(parsed);
|
|
753
779
|
const delta = asRecord(choice.delta);
|
|
754
780
|
const text = contentToText(delta.content);
|
|
@@ -776,6 +802,22 @@ function processCompletionSseLine(line, enqueue, markDone) {
|
|
|
776
802
|
})
|
|
777
803
|
);
|
|
778
804
|
}
|
|
805
|
+
function completionStreamError(event, parsed) {
|
|
806
|
+
const responseError = asRecord(asRecord(parsed.response).error);
|
|
807
|
+
const directError = asRecord(parsed.error);
|
|
808
|
+
const error = Object.keys(directError).length > 0 ? directError : Object.keys(responseError).length > 0 ? responseError : void 0;
|
|
809
|
+
if (error) {
|
|
810
|
+
return error;
|
|
811
|
+
}
|
|
812
|
+
if (event === "error" || parsed.type === "response.failed") {
|
|
813
|
+
return removeUndefined({
|
|
814
|
+
code: contentToText(parsed.code) || void 0,
|
|
815
|
+
message: contentToText(parsed.message) || "Upstream streaming request failed.",
|
|
816
|
+
type: contentToText(parsed.type) || "upstream_stream_error"
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
return void 0;
|
|
820
|
+
}
|
|
779
821
|
function encodeDataSse(data) {
|
|
780
822
|
if (data === "[DONE]") {
|
|
781
823
|
return "data: [DONE]\n\n";
|
|
@@ -1172,10 +1214,19 @@ var DEFAULT_HOST = "127.0.0.1";
|
|
|
1172
1214
|
var DEFAULT_PORT = 4141;
|
|
1173
1215
|
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
|
|
1174
1216
|
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
1217
|
+
var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
|
|
1218
|
+
var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
1219
|
+
var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
|
|
1175
1220
|
var USAGE_CACHE_TTL_MS = 6e4;
|
|
1221
|
+
var RequestBodyTooLargeError = class extends Error {
|
|
1222
|
+
constructor() {
|
|
1223
|
+
super(REQUEST_TOO_LARGE_MESSAGE);
|
|
1224
|
+
this.name = "RequestBodyTooLargeError";
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1176
1227
|
function createHoopilotHandler(options = {}) {
|
|
1177
1228
|
const client = new CopilotClient(options);
|
|
1178
|
-
const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
|
|
1229
|
+
const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
|
|
1179
1230
|
const logger = serverLogger(options);
|
|
1180
1231
|
const metrics = options.metrics ?? new MetricsRegistry();
|
|
1181
1232
|
const readUsage = createUsageReader(client, metrics);
|
|
@@ -1261,6 +1312,12 @@ function createHoopilotHandler(options = {}) {
|
|
|
1261
1312
|
"request body was invalid json"
|
|
1262
1313
|
);
|
|
1263
1314
|
return finish(jsonError(400, "invalid_request_error", message));
|
|
1315
|
+
} else if (error instanceof RequestBodyTooLargeError) {
|
|
1316
|
+
requestLogger.warn(
|
|
1317
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
1318
|
+
"request body exceeded size limit"
|
|
1319
|
+
);
|
|
1320
|
+
return finish(jsonError(413, "request_too_large", message));
|
|
1264
1321
|
} else {
|
|
1265
1322
|
requestLogger.error(
|
|
1266
1323
|
{ err: errorDetails(error), event: "http.request.failed" },
|
|
@@ -1272,10 +1329,10 @@ function createHoopilotHandler(options = {}) {
|
|
|
1272
1329
|
};
|
|
1273
1330
|
}
|
|
1274
1331
|
function startHoopilotServer(options = {}) {
|
|
1275
|
-
const host = options.host ?? options.env?.HOST ?? DEFAULT_HOST;
|
|
1276
|
-
const port = normalizeServerPort(options.port ?? options.env?.PORT ?? DEFAULT_PORT);
|
|
1277
|
-
const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
|
|
1278
|
-
const allowUnauthenticated = options.allowUnauthenticated ?? options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED === "1";
|
|
1332
|
+
const host = options.host ?? envValue(options.env?.HOST) ?? DEFAULT_HOST;
|
|
1333
|
+
const port = normalizeServerPort(options.port ?? envValue(options.env?.PORT) ?? DEFAULT_PORT);
|
|
1334
|
+
const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
|
|
1335
|
+
const allowUnauthenticated = options.allowUnauthenticated ?? envValue(options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED) === "1";
|
|
1279
1336
|
if (!isLoopbackHost(host) && !apiKey && !allowUnauthenticated) {
|
|
1280
1337
|
throw new Error(
|
|
1281
1338
|
"Refusing to listen on a non-loopback host without HOOPILOT_API_KEY. Set an API key or pass --allow-unauthenticated."
|
|
@@ -1293,7 +1350,7 @@ function startHoopilotServer(options = {}) {
|
|
|
1293
1350
|
});
|
|
1294
1351
|
return {
|
|
1295
1352
|
server,
|
|
1296
|
-
url: `http://${host}:${server.port}`
|
|
1353
|
+
url: `http://${urlHost(host)}:${server.port}`
|
|
1297
1354
|
};
|
|
1298
1355
|
}
|
|
1299
1356
|
async function handleModels(client, metrics, signal, logger) {
|
|
@@ -1402,14 +1459,15 @@ function proxyResponse(upstream) {
|
|
|
1402
1459
|
});
|
|
1403
1460
|
}
|
|
1404
1461
|
async function readJson(request) {
|
|
1462
|
+
const text = await readRequestText(request);
|
|
1405
1463
|
try {
|
|
1406
|
-
return asRecord(
|
|
1464
|
+
return asRecord(JSON.parse(text));
|
|
1407
1465
|
} catch {
|
|
1408
1466
|
throw new Error(INVALID_JSON_MESSAGE);
|
|
1409
1467
|
}
|
|
1410
1468
|
}
|
|
1411
1469
|
async function readJsonText(request) {
|
|
1412
|
-
const text = await request
|
|
1470
|
+
const text = await readRequestText(request);
|
|
1413
1471
|
try {
|
|
1414
1472
|
JSON.parse(text);
|
|
1415
1473
|
return text;
|
|
@@ -1417,6 +1475,40 @@ async function readJsonText(request) {
|
|
|
1417
1475
|
throw new Error(INVALID_JSON_MESSAGE);
|
|
1418
1476
|
}
|
|
1419
1477
|
}
|
|
1478
|
+
async function readRequestText(request) {
|
|
1479
|
+
const contentLength = request.headers.get("content-length");
|
|
1480
|
+
if (contentLength) {
|
|
1481
|
+
const declaredBytes = Number(contentLength);
|
|
1482
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > MAX_REQUEST_BODY_BYTES) {
|
|
1483
|
+
throw new RequestBodyTooLargeError();
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
const body = request.body;
|
|
1487
|
+
if (!body) {
|
|
1488
|
+
return "";
|
|
1489
|
+
}
|
|
1490
|
+
const reader = body.getReader();
|
|
1491
|
+
const decoder = new TextDecoder();
|
|
1492
|
+
let bytes = 0;
|
|
1493
|
+
let text = "";
|
|
1494
|
+
try {
|
|
1495
|
+
while (true) {
|
|
1496
|
+
const { done, value } = await reader.read();
|
|
1497
|
+
if (done) {
|
|
1498
|
+
return `${text}${decoder.decode()}`;
|
|
1499
|
+
}
|
|
1500
|
+
bytes += value.byteLength;
|
|
1501
|
+
if (bytes > MAX_REQUEST_BODY_BYTES) {
|
|
1502
|
+
await reader.cancel().catch(() => {
|
|
1503
|
+
});
|
|
1504
|
+
throw new RequestBodyTooLargeError();
|
|
1505
|
+
}
|
|
1506
|
+
text += decoder.decode(value, { stream: true });
|
|
1507
|
+
}
|
|
1508
|
+
} finally {
|
|
1509
|
+
reader.releaseLock();
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1420
1512
|
function jsonResponse(body, status = 200) {
|
|
1421
1513
|
return new Response(JSON.stringify(body), {
|
|
1422
1514
|
headers: {
|
|
@@ -1489,6 +1581,9 @@ function upstreamAuthMessage(message) {
|
|
|
1489
1581
|
function isLoopbackHost(host) {
|
|
1490
1582
|
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
1491
1583
|
}
|
|
1584
|
+
function urlHost(host) {
|
|
1585
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
1586
|
+
}
|
|
1492
1587
|
function isLoopbackOrigin(origin) {
|
|
1493
1588
|
try {
|
|
1494
1589
|
return isLoopbackHost(new URL(origin).hostname.toLowerCase());
|
|
@@ -1596,7 +1691,7 @@ function logRequestCompleted(logger, status, stream, durationMs) {
|
|
|
1596
1691
|
}
|
|
1597
1692
|
function requestIdFor(request) {
|
|
1598
1693
|
const existing = request.headers.get("x-request-id")?.trim();
|
|
1599
|
-
return existing
|
|
1694
|
+
return existing && REQUEST_ID_PATTERN.test(existing) ? existing : crypto.randomUUID();
|
|
1600
1695
|
}
|
|
1601
1696
|
function canonicalApiPath(path) {
|
|
1602
1697
|
const withoutTrailingSlash = path.length > 1 ? path.replace(/\/+$/, "") : path;
|
|
@@ -2297,6 +2392,7 @@ function errorMessage2(error) {
|
|
|
2297
2392
|
}
|
|
2298
2393
|
|
|
2299
2394
|
// src/cli.ts
|
|
2395
|
+
var ALLOWED_COPILOT_API_HOSTS2 = ["api.githubcopilot.com"];
|
|
2300
2396
|
async function main2(argv = Bun.argv.slice(2)) {
|
|
2301
2397
|
cleanupOldBinary();
|
|
2302
2398
|
const command = argv[0];
|
|
@@ -2399,46 +2495,54 @@ function parseArgs(argv) {
|
|
|
2399
2495
|
args.noUpdateCheck = true;
|
|
2400
2496
|
continue;
|
|
2401
2497
|
}
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
if (!value) {
|
|
2405
|
-
throw new Error(`Missing value for ${arg}.`);
|
|
2498
|
+
if (!arg.startsWith("-")) {
|
|
2499
|
+
throw new Error(`Unknown argument: ${arg}.`);
|
|
2406
2500
|
}
|
|
2501
|
+
const [name, inlineValue] = splitOption(arg);
|
|
2407
2502
|
switch (name) {
|
|
2408
2503
|
case "--api-key":
|
|
2409
|
-
args.apiKey =
|
|
2504
|
+
args.apiKey = optionValue(name, inlineValue, rest);
|
|
2410
2505
|
break;
|
|
2411
2506
|
case "--api-key-file":
|
|
2412
|
-
args.apiKey = readApiKeyFile(
|
|
2507
|
+
args.apiKey = readApiKeyFile(optionValue(name, inlineValue, rest));
|
|
2413
2508
|
break;
|
|
2414
2509
|
case "--auth-file":
|
|
2415
|
-
args.authStorePath =
|
|
2510
|
+
args.authStorePath = optionValue(name, inlineValue, rest);
|
|
2416
2511
|
break;
|
|
2417
2512
|
case "--copilot-api-base-url":
|
|
2418
|
-
args.copilotApiBaseUrl =
|
|
2513
|
+
args.copilotApiBaseUrl = optionValue(name, inlineValue, rest);
|
|
2419
2514
|
break;
|
|
2420
2515
|
case "--log-format":
|
|
2421
|
-
args.logFormat = parseLogFormat(
|
|
2516
|
+
args.logFormat = parseLogFormat(optionValue(name, inlineValue, rest));
|
|
2422
2517
|
break;
|
|
2423
2518
|
case "--log-level":
|
|
2424
|
-
args.logLevel = parseLogLevel(
|
|
2519
|
+
args.logLevel = parseLogLevel(optionValue(name, inlineValue, rest));
|
|
2425
2520
|
break;
|
|
2426
2521
|
case "--host":
|
|
2427
|
-
args.host =
|
|
2522
|
+
args.host = optionValue(name, inlineValue, rest);
|
|
2428
2523
|
break;
|
|
2429
2524
|
case "--port":
|
|
2430
|
-
case "-p":
|
|
2525
|
+
case "-p": {
|
|
2526
|
+
const value = optionValue(name, inlineValue, rest);
|
|
2431
2527
|
args.port = Number(value);
|
|
2432
2528
|
if (!Number.isInteger(args.port) || args.port <= 0 || args.port > 65535) {
|
|
2433
2529
|
throw new Error(`Invalid port: ${value}.`);
|
|
2434
2530
|
}
|
|
2435
2531
|
break;
|
|
2532
|
+
}
|
|
2436
2533
|
default:
|
|
2437
2534
|
throw new Error(`Unknown option: ${name}.`);
|
|
2438
2535
|
}
|
|
2439
2536
|
}
|
|
2440
2537
|
return args;
|
|
2441
2538
|
}
|
|
2539
|
+
function optionValue(name, inlineValue, rest) {
|
|
2540
|
+
const value = inlineValue ?? rest.shift();
|
|
2541
|
+
if (!value) {
|
|
2542
|
+
throw new Error(`Missing value for ${name}.`);
|
|
2543
|
+
}
|
|
2544
|
+
return value;
|
|
2545
|
+
}
|
|
2442
2546
|
function splitOption(arg) {
|
|
2443
2547
|
const separator = arg.indexOf("=");
|
|
2444
2548
|
if (separator === -1) {
|
|
@@ -2589,10 +2693,13 @@ function roundQuota(value) {
|
|
|
2589
2693
|
}
|
|
2590
2694
|
async function verifyCopilotOAuthToken(token, options = {}) {
|
|
2591
2695
|
const apiBaseUrl = trimTrailingSlash(
|
|
2592
|
-
options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL
|
|
2696
|
+
options.copilotApiBaseUrl ?? envValue(options.env?.COPILOT_API_BASE_URL) ?? DEFAULT_COPILOT_API_BASE_URL
|
|
2593
2697
|
);
|
|
2594
|
-
|
|
2595
|
-
|
|
2698
|
+
const allowUnsafeUpstream = envValue(options.env?.HOOPILOT_ALLOW_UNSAFE_UPSTREAM) === "1";
|
|
2699
|
+
if (!isTrustedTokenBaseUrl(apiBaseUrl, ALLOWED_COPILOT_API_HOSTS2, allowUnsafeUpstream)) {
|
|
2700
|
+
throw new Error(
|
|
2701
|
+
`Refusing to send the GitHub OAuth token to an untrusted Copilot API host: ${apiBaseUrl}`
|
|
2702
|
+
);
|
|
2596
2703
|
}
|
|
2597
2704
|
const fetcher = options.fetch ?? fetch;
|
|
2598
2705
|
const response = await fetcher(`${apiBaseUrl}/models`, {
|
|
@@ -2700,6 +2807,7 @@ Environment:
|
|
|
2700
2807
|
HOOPILOT_LOG_LEVEL trace, debug, info, warn, error, fatal, or silent
|
|
2701
2808
|
COPILOT_API_BASE_URL
|
|
2702
2809
|
HOOPILOT_GITHUB_API_BASE_URL GitHub REST base for the usage/quota lookup. Default: https://api.github.com
|
|
2810
|
+
HOOPILOT_ALLOW_UNSAFE_UPSTREAM Set to 1 to allow nonstandard HTTPS token hosts
|
|
2703
2811
|
HOOPILOT_NO_UPDATE_CHECK Set to disable update checks (also NO_UPDATE_NOTIFIER)
|
|
2704
2812
|
`;
|
|
2705
2813
|
}
|