@openhoo/hoopilot 2.1.3 → 2.1.5
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 +9 -4
- package/dist/{chunk-CYR6I4C3.js → chunk-4ZG5QEYJ.js} +24 -4
- package/dist/chunk-4ZG5QEYJ.js.map +1 -0
- package/dist/cli.js +286 -55
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +276 -52
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/dist/chunk-CYR6I4C3.js.map +0 -1
package/dist/codexx.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -40,11 +40,11 @@ declare class MetricsRegistry {
|
|
|
40
40
|
renderPrometheus(now?: () => number): string;
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
43
|
+
* Wrap `response`'s body so the client receives unchanged bytes while the same
|
|
44
|
+
* read pass extracts token usage. Returns a new Response carrying the observed
|
|
45
|
+
* body and the original status/headers. Usage extraction never throws into the
|
|
46
|
+
* client stream: a parse failure or an aborted client simply yields no usage.
|
|
47
|
+
* When the body is absent the response is returned untouched.
|
|
48
48
|
*
|
|
49
49
|
* Pass the request's `signal` so a client disconnect cancels the observer
|
|
50
50
|
* branch; combined with the runtime cancelling the client branch, that releases
|
|
@@ -93,6 +93,8 @@ interface CopilotAuthOptions {
|
|
|
93
93
|
env?: NodeJS.ProcessEnv;
|
|
94
94
|
fetch?: FetchLike;
|
|
95
95
|
githubApiBaseUrl?: string;
|
|
96
|
+
upstreamStreamIdleTimeoutMs?: number;
|
|
97
|
+
upstreamTimeoutMs?: number;
|
|
96
98
|
}
|
|
97
99
|
interface CopilotAccess {
|
|
98
100
|
apiBaseUrl: string;
|
package/dist/index.js
CHANGED
|
@@ -2005,6 +2005,14 @@ var COPILOT_USAGE_API_VERSION = "2025-04-01";
|
|
|
2005
2005
|
var EDITOR_PLUGIN_VERSION = "hoopilot/0.1.0";
|
|
2006
2006
|
var EDITOR_VERSION = "Hoopilot/0.1.0";
|
|
2007
2007
|
var HOOPILOT_USER_AGENT = "hoopilot/0.1.0";
|
|
2008
|
+
var DEFAULT_UPSTREAM_TIMEOUT_MS = 12e4;
|
|
2009
|
+
var DEFAULT_UPSTREAM_STREAM_IDLE_TIMEOUT_MS = 12e4;
|
|
2010
|
+
var CopilotUpstreamTimeoutError = class extends Error {
|
|
2011
|
+
constructor(message) {
|
|
2012
|
+
super(message);
|
|
2013
|
+
this.name = "CopilotUpstreamTimeoutError";
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2008
2016
|
function applyCopilotHeaders(headers, token) {
|
|
2009
2017
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
2010
2018
|
headers.set("authorization", `Bearer ${token}`);
|
|
@@ -2057,6 +2065,8 @@ var CopilotClient = class {
|
|
|
2057
2065
|
#allowUnsafeUpstream;
|
|
2058
2066
|
#fetch;
|
|
2059
2067
|
#githubApiBaseUrl;
|
|
2068
|
+
#upstreamStreamIdleTimeoutMs;
|
|
2069
|
+
#upstreamTimeoutMs;
|
|
2060
2070
|
constructor(options = {}) {
|
|
2061
2071
|
this.#auth = new CopilotAuth(options);
|
|
2062
2072
|
this.#allowUnsafeUpstream = envValue(options.env?.HOOPILOT_ALLOW_UNSAFE_UPSTREAM) === "1";
|
|
@@ -2064,6 +2074,18 @@ var CopilotClient = class {
|
|
|
2064
2074
|
this.#githubApiBaseUrl = trimTrailingSlash(
|
|
2065
2075
|
options.githubApiBaseUrl ?? envValue(options.env?.HOOPILOT_GITHUB_API_BASE_URL) ?? DEFAULT_GITHUB_API_BASE_URL
|
|
2066
2076
|
);
|
|
2077
|
+
this.#upstreamTimeoutMs = parseTimeoutMs(
|
|
2078
|
+
options.upstreamTimeoutMs,
|
|
2079
|
+
options.env?.HOOPILOT_UPSTREAM_TIMEOUT_MS,
|
|
2080
|
+
DEFAULT_UPSTREAM_TIMEOUT_MS,
|
|
2081
|
+
"HOOPILOT_UPSTREAM_TIMEOUT_MS"
|
|
2082
|
+
);
|
|
2083
|
+
this.#upstreamStreamIdleTimeoutMs = parseTimeoutMs(
|
|
2084
|
+
options.upstreamStreamIdleTimeoutMs,
|
|
2085
|
+
options.env?.HOOPILOT_UPSTREAM_STREAM_IDLE_TIMEOUT_MS,
|
|
2086
|
+
DEFAULT_UPSTREAM_STREAM_IDLE_TIMEOUT_MS,
|
|
2087
|
+
"HOOPILOT_UPSTREAM_STREAM_IDLE_TIMEOUT_MS"
|
|
2088
|
+
);
|
|
2067
2089
|
}
|
|
2068
2090
|
/**
|
|
2069
2091
|
* Fetch the Copilot account's quota / premium-request usage from the GitHub
|
|
@@ -2082,7 +2104,7 @@ var CopilotClient = class {
|
|
|
2082
2104
|
}
|
|
2083
2105
|
const access = await this.#auth.getAccess();
|
|
2084
2106
|
const headers = applyGithubApiHeaders(new Headers(), access.token);
|
|
2085
|
-
return this.#
|
|
2107
|
+
return this.#fetchWithTimeout(`${this.#githubApiBaseUrl}/copilot_internal/user`, {
|
|
2086
2108
|
headers,
|
|
2087
2109
|
method: "GET",
|
|
2088
2110
|
signal
|
|
@@ -2129,12 +2151,139 @@ var CopilotClient = class {
|
|
|
2129
2151
|
);
|
|
2130
2152
|
}
|
|
2131
2153
|
const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
|
|
2132
|
-
return this.#
|
|
2154
|
+
return this.#fetchWithTimeout(`${access.apiBaseUrl}${path}`, {
|
|
2133
2155
|
...init,
|
|
2134
2156
|
headers
|
|
2135
2157
|
});
|
|
2136
2158
|
}
|
|
2159
|
+
async #fetchWithTimeout(input, init) {
|
|
2160
|
+
const timeout = abortSignalWithTimeout(init.signal ?? void 0, this.#upstreamTimeoutMs);
|
|
2161
|
+
try {
|
|
2162
|
+
const response = await this.#fetch(input, {
|
|
2163
|
+
...init,
|
|
2164
|
+
signal: timeout.signal
|
|
2165
|
+
});
|
|
2166
|
+
return responseWithStreamIdleTimeout(response, this.#upstreamStreamIdleTimeoutMs, input);
|
|
2167
|
+
} catch (error) {
|
|
2168
|
+
if (timeout.timedOut()) {
|
|
2169
|
+
throw new CopilotUpstreamTimeoutError(
|
|
2170
|
+
`Copilot upstream request timed out after ${this.#upstreamTimeoutMs} ms before response headers arrived.`
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
throw error;
|
|
2174
|
+
} finally {
|
|
2175
|
+
timeout.cleanup();
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2137
2178
|
};
|
|
2179
|
+
function parseTimeoutMs(optionValue, envRaw, fallback, name) {
|
|
2180
|
+
const raw = optionValue ?? envValue(envRaw);
|
|
2181
|
+
if (raw === void 0) {
|
|
2182
|
+
return fallback;
|
|
2183
|
+
}
|
|
2184
|
+
const value = typeof raw === "number" ? raw : Number(raw);
|
|
2185
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
2186
|
+
throw new Error(`${name} must be a non-negative integer number of milliseconds.`);
|
|
2187
|
+
}
|
|
2188
|
+
return value;
|
|
2189
|
+
}
|
|
2190
|
+
function abortSignalWithTimeout(parent, timeoutMs) {
|
|
2191
|
+
if (timeoutMs === 0) {
|
|
2192
|
+
return { cleanup: () => {
|
|
2193
|
+
}, signal: parent, timedOut: () => false };
|
|
2194
|
+
}
|
|
2195
|
+
const controller = new AbortController();
|
|
2196
|
+
let timedOut = false;
|
|
2197
|
+
const timer = setTimeout(() => {
|
|
2198
|
+
if (controller.signal.aborted) {
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
timedOut = true;
|
|
2202
|
+
controller.abort(
|
|
2203
|
+
new CopilotUpstreamTimeoutError(`Copilot upstream request timed out after ${timeoutMs} ms.`)
|
|
2204
|
+
);
|
|
2205
|
+
}, timeoutMs);
|
|
2206
|
+
const onAbort = () => controller.abort(parent?.reason);
|
|
2207
|
+
if (parent?.aborted) {
|
|
2208
|
+
controller.abort(parent.reason);
|
|
2209
|
+
} else {
|
|
2210
|
+
parent?.addEventListener("abort", onAbort, { once: true });
|
|
2211
|
+
}
|
|
2212
|
+
return {
|
|
2213
|
+
cleanup: () => {
|
|
2214
|
+
clearTimeout(timer);
|
|
2215
|
+
parent?.removeEventListener("abort", onAbort);
|
|
2216
|
+
},
|
|
2217
|
+
signal: controller.signal,
|
|
2218
|
+
timedOut: () => timedOut
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
function responseWithStreamIdleTimeout(response, idleTimeoutMs, input) {
|
|
2222
|
+
if (!response.body || idleTimeoutMs === 0) {
|
|
2223
|
+
return response;
|
|
2224
|
+
}
|
|
2225
|
+
return new Response(streamWithIdleTimeout(response.body, idleTimeoutMs, input), {
|
|
2226
|
+
headers: response.headers,
|
|
2227
|
+
status: response.status,
|
|
2228
|
+
statusText: response.statusText
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
function streamWithIdleTimeout(body, idleTimeoutMs, input) {
|
|
2232
|
+
const reader = body.getReader();
|
|
2233
|
+
let released = false;
|
|
2234
|
+
const release = () => {
|
|
2235
|
+
if (!released) {
|
|
2236
|
+
released = true;
|
|
2237
|
+
reader.releaseLock();
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
return new ReadableStream({
|
|
2241
|
+
async pull(controller) {
|
|
2242
|
+
let timer;
|
|
2243
|
+
const read = reader.read();
|
|
2244
|
+
read.catch(() => {
|
|
2245
|
+
});
|
|
2246
|
+
try {
|
|
2247
|
+
const result = await Promise.race([
|
|
2248
|
+
read,
|
|
2249
|
+
new Promise((_, reject) => {
|
|
2250
|
+
timer = setTimeout(() => {
|
|
2251
|
+
reject(
|
|
2252
|
+
new CopilotUpstreamTimeoutError(
|
|
2253
|
+
`Copilot upstream stream was idle for ${idleTimeoutMs} ms while reading ${input}.`
|
|
2254
|
+
)
|
|
2255
|
+
);
|
|
2256
|
+
}, idleTimeoutMs);
|
|
2257
|
+
})
|
|
2258
|
+
]);
|
|
2259
|
+
if (timer) {
|
|
2260
|
+
clearTimeout(timer);
|
|
2261
|
+
}
|
|
2262
|
+
if (result.done) {
|
|
2263
|
+
controller.close();
|
|
2264
|
+
release();
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
controller.enqueue(result.value);
|
|
2268
|
+
} catch (error) {
|
|
2269
|
+
if (timer) {
|
|
2270
|
+
clearTimeout(timer);
|
|
2271
|
+
}
|
|
2272
|
+
await reader.cancel(error).catch(() => {
|
|
2273
|
+
});
|
|
2274
|
+
controller.error(error);
|
|
2275
|
+
release();
|
|
2276
|
+
}
|
|
2277
|
+
},
|
|
2278
|
+
async cancel(reason) {
|
|
2279
|
+
try {
|
|
2280
|
+
await reader.cancel(reason);
|
|
2281
|
+
} finally {
|
|
2282
|
+
release();
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2138
2287
|
function normalizeCopilotUsage(body) {
|
|
2139
2288
|
const record = asRecord(body);
|
|
2140
2289
|
const quotas = {};
|
|
@@ -2871,17 +3020,15 @@ function observeResponseUsage(response, fallbackModel, onUsage, signal, onOutcom
|
|
|
2871
3020
|
if (!body) {
|
|
2872
3021
|
return response;
|
|
2873
3022
|
}
|
|
2874
|
-
const [clientBranch, observerBranch] = body.tee();
|
|
2875
3023
|
const isSse = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
|
|
2876
|
-
|
|
2877
|
-
(
|
|
3024
|
+
return new Response(
|
|
3025
|
+
streamWithUsageObservation(body, isSse, fallbackModel, onUsage, signal, onOutcome),
|
|
3026
|
+
{
|
|
3027
|
+
headers: response.headers,
|
|
3028
|
+
status: response.status,
|
|
3029
|
+
statusText: response.statusText
|
|
2878
3030
|
}
|
|
2879
3031
|
);
|
|
2880
|
-
return new Response(clientBranch, {
|
|
2881
|
-
headers: response.headers,
|
|
2882
|
-
status: response.status,
|
|
2883
|
-
statusText: response.statusText
|
|
2884
|
-
});
|
|
2885
3032
|
}
|
|
2886
3033
|
function recordResponseTextUsage(text, isSse, fallbackModel, onUsage, onOutcome) {
|
|
2887
3034
|
const accumulator = createUsageAccumulator(fallbackModel, onUsage, onOutcome);
|
|
@@ -2897,13 +3044,16 @@ function recordResponseTextUsage(text, isSse, fallbackModel, onUsage, onOutcome)
|
|
|
2897
3044
|
}
|
|
2898
3045
|
accumulator.finish();
|
|
2899
3046
|
}
|
|
2900
|
-
|
|
3047
|
+
function streamWithUsageObservation(stream, isSse, fallbackModel, onUsage, signal, onOutcome) {
|
|
2901
3048
|
const reader = stream.getReader();
|
|
3049
|
+
let aborted = signal?.aborted ?? false;
|
|
3050
|
+
let released = false;
|
|
2902
3051
|
const onAbort = () => {
|
|
3052
|
+
aborted = true;
|
|
2903
3053
|
reader.cancel().catch(() => {
|
|
2904
3054
|
});
|
|
2905
3055
|
};
|
|
2906
|
-
if (
|
|
3056
|
+
if (aborted) {
|
|
2907
3057
|
reader.cancel().catch(() => {
|
|
2908
3058
|
});
|
|
2909
3059
|
} else {
|
|
@@ -2911,7 +3061,7 @@ async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal, onOut
|
|
|
2911
3061
|
}
|
|
2912
3062
|
const decoder = new TextDecoder();
|
|
2913
3063
|
const guardedOutcome = onOutcome ? (extracted) => {
|
|
2914
|
-
if (!
|
|
3064
|
+
if (!aborted) {
|
|
2915
3065
|
onOutcome(extracted);
|
|
2916
3066
|
}
|
|
2917
3067
|
} : void 0;
|
|
@@ -2919,33 +3069,40 @@ async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal, onOut
|
|
|
2919
3069
|
let buffer = "";
|
|
2920
3070
|
let bufferedBytes = 0;
|
|
2921
3071
|
let overflowed = false;
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3072
|
+
const release = () => {
|
|
3073
|
+
if (released) {
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
released = true;
|
|
3077
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3078
|
+
reader.releaseLock();
|
|
3079
|
+
};
|
|
3080
|
+
const observeChunk = (chunkBytes) => {
|
|
3081
|
+
const chunk = decoder.decode(chunkBytes, { stream: true });
|
|
3082
|
+
if (isSse) {
|
|
3083
|
+
buffer += chunk;
|
|
3084
|
+
const lines = buffer.split(/\r?\n/);
|
|
3085
|
+
buffer = lines.pop() ?? "";
|
|
3086
|
+
for (const line of lines) {
|
|
3087
|
+
considerSseLine(line, accumulator.consider);
|
|
2927
3088
|
}
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
buffer += chunk;
|
|
2931
|
-
const lines = buffer.split(/\r?\n/);
|
|
2932
|
-
buffer = lines.pop() ?? "";
|
|
2933
|
-
for (const line of lines) {
|
|
2934
|
-
considerSseLine(line, accumulator.consider);
|
|
2935
|
-
}
|
|
2936
|
-
if (buffer.length > USAGE_BUFFER_LIMIT_BYTES) {
|
|
2937
|
-
buffer = "";
|
|
2938
|
-
}
|
|
2939
|
-
} else if (!overflowed) {
|
|
2940
|
-
bufferedBytes += result.value.byteLength;
|
|
2941
|
-
if (bufferedBytes > USAGE_BUFFER_LIMIT_BYTES) {
|
|
2942
|
-
overflowed = true;
|
|
2943
|
-
buffer = "";
|
|
2944
|
-
} else {
|
|
2945
|
-
buffer += chunk;
|
|
2946
|
-
}
|
|
3089
|
+
if (buffer.length > USAGE_BUFFER_LIMIT_BYTES) {
|
|
3090
|
+
buffer = "";
|
|
2947
3091
|
}
|
|
3092
|
+
return;
|
|
2948
3093
|
}
|
|
3094
|
+
if (overflowed) {
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
bufferedBytes += chunkBytes.byteLength;
|
|
3098
|
+
if (bufferedBytes > USAGE_BUFFER_LIMIT_BYTES) {
|
|
3099
|
+
overflowed = true;
|
|
3100
|
+
buffer = "";
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
buffer += chunk;
|
|
3104
|
+
};
|
|
3105
|
+
const finishObservation = () => {
|
|
2949
3106
|
const finalBuffer = buffer + decoder.decode();
|
|
2950
3107
|
if (isSse) {
|
|
2951
3108
|
if (finalBuffer) {
|
|
@@ -2957,11 +3114,41 @@ async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal, onOut
|
|
|
2957
3114
|
accumulator.consider(parsed);
|
|
2958
3115
|
}
|
|
2959
3116
|
}
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
}
|
|
2964
|
-
|
|
3117
|
+
if (!aborted) {
|
|
3118
|
+
safeFinishAccumulator(accumulator);
|
|
3119
|
+
}
|
|
3120
|
+
};
|
|
3121
|
+
return new ReadableStream({
|
|
3122
|
+
async pull(controller) {
|
|
3123
|
+
const result = await reader.read().catch((error) => {
|
|
3124
|
+
release();
|
|
3125
|
+
controller.error(error);
|
|
3126
|
+
return void 0;
|
|
3127
|
+
});
|
|
3128
|
+
if (!result) {
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
if (result.done) {
|
|
3132
|
+
finishObservation();
|
|
3133
|
+
controller.close();
|
|
3134
|
+
release();
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
try {
|
|
3138
|
+
observeChunk(result.value);
|
|
3139
|
+
} catch {
|
|
3140
|
+
}
|
|
3141
|
+
controller.enqueue(result.value);
|
|
3142
|
+
},
|
|
3143
|
+
async cancel(reason) {
|
|
3144
|
+
aborted = true;
|
|
3145
|
+
try {
|
|
3146
|
+
await reader.cancel(reason);
|
|
3147
|
+
} finally {
|
|
3148
|
+
release();
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
});
|
|
2965
3152
|
}
|
|
2966
3153
|
function createUsageAccumulator(fallbackModel, onUsage, onOutcome) {
|
|
2967
3154
|
let model = fallbackModel;
|
|
@@ -2986,6 +3173,12 @@ function createUsageAccumulator(fallbackModel, onUsage, onOutcome) {
|
|
|
2986
3173
|
}
|
|
2987
3174
|
};
|
|
2988
3175
|
}
|
|
3176
|
+
function safeFinishAccumulator(accumulator) {
|
|
3177
|
+
try {
|
|
3178
|
+
accumulator.finish();
|
|
3179
|
+
} catch {
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
2989
3182
|
function considerSseLine(line, consider) {
|
|
2990
3183
|
const trimmed = line.trim();
|
|
2991
3184
|
if (!trimmed.startsWith("data:")) {
|
|
@@ -3916,7 +4109,18 @@ async function getVersion() {
|
|
|
3916
4109
|
var DEFAULT_HOST = "127.0.0.1";
|
|
3917
4110
|
var DEFAULT_PORT = 4141;
|
|
3918
4111
|
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Cross-origin browser requests are blocked unless the Origin is loopback or listed in HOOPILOT_ALLOWED_ORIGINS.";
|
|
3919
|
-
var
|
|
4112
|
+
var MIN_NON_LOOPBACK_API_KEY_LENGTH = 24;
|
|
4113
|
+
var WELL_KNOWN_DEMO_API_KEYS = /* @__PURE__ */ new Set([
|
|
4114
|
+
"changeme",
|
|
4115
|
+
"demo",
|
|
4116
|
+
"example",
|
|
4117
|
+
"hoopilot",
|
|
4118
|
+
"local-key",
|
|
4119
|
+
"password",
|
|
4120
|
+
"password123",
|
|
4121
|
+
"secret",
|
|
4122
|
+
"test"
|
|
4123
|
+
]);
|
|
3920
4124
|
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
3921
4125
|
var JSON_OBJECT_MESSAGE = "Request body must be a JSON object.";
|
|
3922
4126
|
var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
|
|
@@ -4089,6 +4293,13 @@ function buildApp(deps) {
|
|
|
4089
4293
|
);
|
|
4090
4294
|
return jsonError(413, "request_too_large", message);
|
|
4091
4295
|
}
|
|
4296
|
+
if (error instanceof CopilotUpstreamTimeoutError) {
|
|
4297
|
+
logger.warn(
|
|
4298
|
+
{ err: errorDetails(error), event: "copilot.request.timeout" },
|
|
4299
|
+
"copilot upstream request timed out"
|
|
4300
|
+
);
|
|
4301
|
+
return jsonError(504, "copilot_timeout", message);
|
|
4302
|
+
}
|
|
4092
4303
|
logger.error({ err: errorDetails(error), event: "http.request.failed" }, "request failed");
|
|
4093
4304
|
return jsonError(500, "internal_error", message);
|
|
4094
4305
|
}).get("/", () => jsonResponse({ name: "hoopilot", object: "health", status: "ok" })).get("/healthz", () => jsonResponse({ name: "hoopilot", object: "health", status: "ok" })).get("/metrics", () => metricsResponse(metrics)).get("/v1/usage", ({ request }) => handleUsage(metrics, readUsage, request.signal)).get(
|
|
@@ -4187,10 +4398,9 @@ function startHoopilotServer(options = {}) {
|
|
|
4187
4398
|
"Refusing to listen on a non-loopback host without HOOPILOT_API_KEY. Set an API key or pass --allow-unauthenticated."
|
|
4188
4399
|
);
|
|
4189
4400
|
}
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
);
|
|
4401
|
+
const rejection = apiKey ? apiKeyRejectionReason(apiKey) : void 0;
|
|
4402
|
+
if (rejection) {
|
|
4403
|
+
throw new Error(`Refusing to listen on a non-loopback host: ${rejection}`);
|
|
4194
4404
|
}
|
|
4195
4405
|
}
|
|
4196
4406
|
const server = Bun.serve({
|
|
@@ -4488,12 +4698,16 @@ async function readRequestText(request) {
|
|
|
4488
4698
|
const reader = body.getReader();
|
|
4489
4699
|
const decoder = new TextDecoder();
|
|
4490
4700
|
let bytes = 0;
|
|
4491
|
-
|
|
4701
|
+
const chunks = [];
|
|
4492
4702
|
try {
|
|
4493
4703
|
while (true) {
|
|
4494
4704
|
const { done, value } = await reader.read();
|
|
4495
4705
|
if (done) {
|
|
4496
|
-
|
|
4706
|
+
const tail = decoder.decode();
|
|
4707
|
+
if (tail) {
|
|
4708
|
+
chunks.push(tail);
|
|
4709
|
+
}
|
|
4710
|
+
return chunks.join("");
|
|
4497
4711
|
}
|
|
4498
4712
|
bytes += value.byteLength;
|
|
4499
4713
|
if (bytes > MAX_REQUEST_BODY_BYTES) {
|
|
@@ -4501,7 +4715,7 @@ async function readRequestText(request) {
|
|
|
4501
4715
|
});
|
|
4502
4716
|
throw new RequestBodyTooLargeError();
|
|
4503
4717
|
}
|
|
4504
|
-
|
|
4718
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
4505
4719
|
}
|
|
4506
4720
|
} finally {
|
|
4507
4721
|
reader.releaseLock();
|
|
@@ -4598,8 +4812,18 @@ function resolveCorsAllowOrigin(origin, allowedOrigins) {
|
|
|
4598
4812
|
}
|
|
4599
4813
|
return isAllowedOrigin(origin, allowedOrigins) ? origin : void 0;
|
|
4600
4814
|
}
|
|
4601
|
-
function
|
|
4602
|
-
|
|
4815
|
+
function apiKeyRejectionReason(apiKey) {
|
|
4816
|
+
const normalized = apiKey.trim();
|
|
4817
|
+
if (WELL_KNOWN_DEMO_API_KEYS.has(normalized.toLowerCase())) {
|
|
4818
|
+
return "HOOPILOT_API_KEY is a well-known demo value. Set a strong, unique API key.";
|
|
4819
|
+
}
|
|
4820
|
+
if (normalized.length < MIN_NON_LOOPBACK_API_KEY_LENGTH) {
|
|
4821
|
+
return `HOOPILOT_API_KEY must be at least ${MIN_NON_LOOPBACK_API_KEY_LENGTH} characters when listening on a non-loopback host.`;
|
|
4822
|
+
}
|
|
4823
|
+
if (/^(.)\1+$/.test(normalized)) {
|
|
4824
|
+
return "HOOPILOT_API_KEY must not be a repeated single character. Set a strong, unique API key.";
|
|
4825
|
+
}
|
|
4826
|
+
return void 0;
|
|
4603
4827
|
}
|
|
4604
4828
|
function isUpstreamAuthStatus(status) {
|
|
4605
4829
|
return status === 401 || status === 403;
|