@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/dist/codexx.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  buildCodexxInvocation,
4
4
  main,
5
5
  verifyCodexxModel
6
- } from "./chunk-TEDEVCKM.js";
6
+ } from "./chunk-7GSQVYYT.js";
7
7
  export {
8
8
  buildCodexxInvocation,
9
9
  main,
package/dist/index.cjs CHANGED
@@ -71,6 +71,56 @@ module.exports = __toCommonJS(index_exports);
71
71
  // src/auth-store.ts
72
72
  var import_node_fs = require("fs");
73
73
  var import_node_path = require("path");
74
+
75
+ // src/util.ts
76
+ function trimTrailingSlash(value) {
77
+ return value.replace(/\/+$/, "");
78
+ }
79
+ function envValue(value) {
80
+ const trimmed = value?.trim();
81
+ return trimmed ? trimmed : void 0;
82
+ }
83
+ function isTrustedTokenBaseUrl(rawUrl, allowedHttpsHosts, allowUnsafeHttps = false) {
84
+ const url = parseUrl(rawUrl);
85
+ if (!url) {
86
+ return false;
87
+ }
88
+ if (url.username || url.password || url.search || url.hash) {
89
+ return false;
90
+ }
91
+ if (url.pathname !== "" && url.pathname !== "/") {
92
+ return false;
93
+ }
94
+ if (isLoopbackHttpUrl(url)) {
95
+ return true;
96
+ }
97
+ if (url.protocol !== "https:") {
98
+ return false;
99
+ }
100
+ const host = url.hostname.toLowerCase();
101
+ return allowedHttpsHosts.includes(host) || allowUnsafeHttps;
102
+ }
103
+ function parseUrl(rawUrl) {
104
+ let url;
105
+ try {
106
+ url = new URL(rawUrl);
107
+ } catch {
108
+ return void 0;
109
+ }
110
+ return url;
111
+ }
112
+ function isLoopbackHttpUrl(url) {
113
+ return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1" || url.hostname === "[::1]");
114
+ }
115
+ async function truncatedResponseText(response, max = 500) {
116
+ const text = await response.text();
117
+ return text.slice(0, max);
118
+ }
119
+ function asRecord(value) {
120
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
121
+ }
122
+
123
+ // src/auth-store.ts
74
124
  var StoredCopilotAuthError = class extends Error {
75
125
  constructor(message) {
76
126
  super(message);
@@ -78,10 +128,25 @@ var StoredCopilotAuthError = class extends Error {
78
128
  }
79
129
  };
80
130
  function authStorePath(env = process.env) {
81
- if (env.HOOPILOT_AUTH_FILE) {
82
- return env.HOOPILOT_AUTH_FILE;
131
+ const explicit = envValue(env.HOOPILOT_AUTH_FILE);
132
+ if (explicit) {
133
+ return explicit;
134
+ }
135
+ const xdg = envValue(env.XDG_CONFIG_HOME);
136
+ if (xdg) {
137
+ return (0, import_node_path.join)(xdg, "hoopilot", "auth.json");
138
+ }
139
+ const appdata = envValue(env.APPDATA);
140
+ if (appdata) {
141
+ return (0, import_node_path.join)(appdata, "hoopilot", "auth.json");
142
+ }
143
+ const home = envValue(env.HOME);
144
+ if (!home) {
145
+ throw new StoredCopilotAuthError(
146
+ "Cannot resolve Hoopilot auth file path without HOOPILOT_AUTH_FILE, XDG_CONFIG_HOME, APPDATA, or HOME."
147
+ );
83
148
  }
84
- const base = env.XDG_CONFIG_HOME ?? env.APPDATA ?? (env.HOME ? (0, import_node_path.join)(env.HOME, ".config") : (0, import_node_path.join)(process.cwd(), ".config"));
149
+ const base = (0, import_node_path.join)(home, ".config");
85
150
  return (0, import_node_path.join)(base, "hoopilot", "auth.json");
86
151
  }
87
152
  function readStoredCopilotAuth(path = authStorePath()) {
@@ -138,30 +203,6 @@ function writeStoredCopilotAuth(auth, path = authStorePath()) {
138
203
  }
139
204
  }
140
205
 
141
- // src/util.ts
142
- function trimTrailingSlash(value) {
143
- return value.replace(/\/+$/, "");
144
- }
145
- function isHttpsOrLoopbackUrl(rawUrl) {
146
- let url;
147
- try {
148
- url = new URL(rawUrl);
149
- } catch {
150
- return false;
151
- }
152
- if (url.protocol === "https:") {
153
- return true;
154
- }
155
- return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1" || url.hostname === "[::1]");
156
- }
157
- async function truncatedResponseText(response, max = 500) {
158
- const text = await response.text();
159
- return text.slice(0, max);
160
- }
161
- function asRecord(value) {
162
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
163
- }
164
-
165
206
  // src/auth.ts
166
207
  var DEFAULT_COPILOT_API_BASE_URL = "https://api.githubcopilot.com";
167
208
  var REFRESH_SKEW_MS = 6e4;
@@ -175,11 +216,15 @@ var CopilotAuthError = class extends Error {
175
216
  var CopilotAuth = class {
176
217
  #authStorePath;
177
218
  #copilotApiBaseUrl;
219
+ #hasCopilotApiBaseUrlOverride;
178
220
  #cachedAccess;
179
221
  constructor(options = {}) {
180
- this.#authStorePath = options.authStorePath ?? options.env?.HOOPILOT_AUTH_FILE;
222
+ const envAuthStorePath = envValue(options.env?.HOOPILOT_AUTH_FILE);
223
+ const envCopilotApiBaseUrl = envValue(options.env?.COPILOT_API_BASE_URL);
224
+ this.#authStorePath = options.authStorePath ?? envAuthStorePath;
225
+ this.#hasCopilotApiBaseUrlOverride = Boolean(options.copilotApiBaseUrl ?? envCopilotApiBaseUrl);
181
226
  this.#copilotApiBaseUrl = trimTrailingSlash(
182
- options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL
227
+ options.copilotApiBaseUrl ?? envCopilotApiBaseUrl ?? DEFAULT_COPILOT_API_BASE_URL
183
228
  );
184
229
  }
185
230
  async getAccess() {
@@ -197,7 +242,9 @@ var CopilotAuth = class {
197
242
  }
198
243
  if (stored) {
199
244
  return this.#cacheAccess({
200
- apiBaseUrl: trimTrailingSlash(stored.apiBaseUrl ?? this.#copilotApiBaseUrl),
245
+ apiBaseUrl: trimTrailingSlash(
246
+ this.#hasCopilotApiBaseUrlOverride ? this.#copilotApiBaseUrl : stored.apiBaseUrl ?? this.#copilotApiBaseUrl
247
+ ),
201
248
  expiresAtMs: Date.now() + STORED_TOKEN_TTL_MS,
202
249
  source: "github-copilot-oauth",
203
250
  token: stored.token
@@ -215,6 +262,8 @@ var CopilotAuth = class {
215
262
 
216
263
  // src/copilot.ts
217
264
  var DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
265
+ var ALLOWED_COPILOT_API_HOSTS = ["api.githubcopilot.com"];
266
+ var ALLOWED_GITHUB_API_HOSTS = ["api.github.com"];
218
267
  var COPILOT_USAGE_API_VERSION = "2025-04-01";
219
268
  function applyCopilotHeaders(headers, token) {
220
269
  headers.set("accept", headers.get("accept") ?? "application/json");
@@ -238,13 +287,15 @@ function applyGithubApiHeaders(headers, token) {
238
287
  }
239
288
  var CopilotClient = class {
240
289
  #auth;
290
+ #allowUnsafeUpstream;
241
291
  #fetch;
242
292
  #githubApiBaseUrl;
243
293
  constructor(options = {}) {
244
294
  this.#auth = new CopilotAuth(options);
295
+ this.#allowUnsafeUpstream = envValue(options.env?.HOOPILOT_ALLOW_UNSAFE_UPSTREAM) === "1";
245
296
  this.#fetch = options.fetch ?? fetch;
246
297
  this.#githubApiBaseUrl = trimTrailingSlash(
247
- options.githubApiBaseUrl ?? options.env?.HOOPILOT_GITHUB_API_BASE_URL ?? DEFAULT_GITHUB_API_BASE_URL
298
+ options.githubApiBaseUrl ?? envValue(options.env?.HOOPILOT_GITHUB_API_BASE_URL) ?? DEFAULT_GITHUB_API_BASE_URL
248
299
  );
249
300
  }
250
301
  /**
@@ -253,9 +304,13 @@ var CopilotClient = class {
253
304
  * accepted directly here — no Copilot token exchange is required to read quota.
254
305
  */
255
306
  async usage(signal) {
256
- if (!isHttpsOrLoopbackUrl(this.#githubApiBaseUrl)) {
307
+ if (!isTrustedTokenBaseUrl(
308
+ this.#githubApiBaseUrl,
309
+ ALLOWED_GITHUB_API_HOSTS,
310
+ this.#allowUnsafeUpstream
311
+ )) {
257
312
  throw new Error(
258
- `Refusing to send the GitHub OAuth token to a non-HTTPS host: ${this.#githubApiBaseUrl}`
313
+ `Refusing to send the GitHub OAuth token to an untrusted GitHub API host: ${this.#githubApiBaseUrl}`
259
314
  );
260
315
  }
261
316
  const access = await this.#auth.getAccess();
@@ -297,9 +352,13 @@ var CopilotClient = class {
297
352
  }
298
353
  async fetchCopilot(path, init) {
299
354
  const access = await this.#auth.getAccess();
300
- if (!isHttpsOrLoopbackUrl(access.apiBaseUrl)) {
355
+ if (!isTrustedTokenBaseUrl(
356
+ access.apiBaseUrl,
357
+ ALLOWED_COPILOT_API_HOSTS,
358
+ this.#allowUnsafeUpstream
359
+ )) {
301
360
  throw new Error(
302
- `Refusing to send the GitHub OAuth token to a non-HTTPS host: ${access.apiBaseUrl}`
361
+ `Refusing to send the GitHub OAuth token to an untrusted Copilot API host: ${access.apiBaseUrl}`
303
362
  );
304
363
  }
305
364
  const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
@@ -385,9 +444,9 @@ async function githubCopilotDeviceLogin(options = {}) {
385
444
  const fetcher = options.fetch ?? fetch;
386
445
  const sleeper = options.sleep ?? import_promises.setTimeout;
387
446
  const domain = normalizeDomain(
388
- options.domain ?? env.HOOPILOT_GITHUB_DOMAIN ?? DEFAULT_GITHUB_DOMAIN
447
+ options.domain ?? envValue(env.HOOPILOT_GITHUB_DOMAIN) ?? DEFAULT_GITHUB_DOMAIN
389
448
  );
390
- const clientId = options.clientId ?? env.HOOPILOT_GITHUB_CLIENT_ID ?? env.COPILOT_GITHUB_CLIENT_ID ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
449
+ const clientId = options.clientId ?? envValue(env.HOOPILOT_GITHUB_CLIENT_ID) ?? envValue(env.COPILOT_GITHUB_CLIENT_ID) ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
391
450
  const device = await requestDeviceCode(fetcher, domain, clientId);
392
451
  const verificationUrl = device.verification_uri;
393
452
  const userCode = device.user_code;
@@ -545,8 +604,8 @@ var noopLogger = {
545
604
  };
546
605
  function createHoopilotLogger(options = {}) {
547
606
  const env = options.env ?? process.env;
548
- const level = parseLogLevel(options.level ?? env.HOOPILOT_LOG_LEVEL);
549
- const format = parseLogFormat(options.format ?? env.HOOPILOT_LOG_FORMAT);
607
+ const level = parseLogLevel(options.level ?? envValue(env.HOOPILOT_LOG_LEVEL));
608
+ const format = parseLogFormat(options.format ?? envValue(env.HOOPILOT_LOG_FORMAT));
550
609
  const pinoOptions = {
551
610
  base: {
552
611
  service: "hoopilot",
@@ -596,7 +655,7 @@ function parseLogLevel(value) {
596
655
  }
597
656
  function shouldCreateLogger(options) {
598
657
  return Boolean(
599
- options.logger || options.logFormat || options.logLevel || options.env?.HOOPILOT_LOG_FORMAT || options.env?.HOOPILOT_LOG_LEVEL
658
+ options.logger || options.logFormat || options.logLevel || envValue(options.env?.HOOPILOT_LOG_FORMAT) || envValue(options.env?.HOOPILOT_LOG_LEVEL)
600
659
  );
601
660
  }
602
661
  function errorDetails(error) {
@@ -715,12 +774,15 @@ function completionStreamFromChatStream(chatStream) {
715
774
  const encoder = new TextEncoder();
716
775
  const decoder = new TextDecoder();
717
776
  let buffer = "";
718
- let sawDone = false;
777
+ let sawTerminalEvent = false;
719
778
  return new ReadableStream({
720
779
  async start(controller) {
721
780
  const enqueue = (data) => {
722
781
  controller.enqueue(encoder.encode(encodeDataSse(data)));
723
782
  };
783
+ const markTerminal = () => {
784
+ sawTerminalEvent = true;
785
+ };
724
786
  const reader = chatStream.getReader();
725
787
  try {
726
788
  while (true) {
@@ -729,20 +791,17 @@ function completionStreamFromChatStream(chatStream) {
729
791
  break;
730
792
  }
731
793
  buffer += decoder.decode(result.value, { stream: true });
732
- const lines = buffer.split(/\r?\n/);
733
- buffer = lines.pop() ?? "";
734
- for (const line of lines) {
735
- processCompletionSseLine(line, enqueue, () => {
736
- sawDone = true;
737
- });
794
+ const blocks = buffer.split(/\r?\n\r?\n/);
795
+ buffer = blocks.pop() ?? "";
796
+ for (const block of blocks) {
797
+ processCompletionSseBlock(block, enqueue, markTerminal);
738
798
  }
739
799
  }
740
- if (buffer) {
741
- processCompletionSseLine(buffer, enqueue, () => {
742
- sawDone = true;
743
- });
800
+ const tail = `${buffer}${decoder.decode()}`;
801
+ if (tail.trim()) {
802
+ processCompletionSseBlock(tail, enqueue, markTerminal);
744
803
  }
745
- if (!sawDone) {
804
+ if (!sawTerminalEvent) {
746
805
  enqueue("[DONE]");
747
806
  }
748
807
  controller.close();
@@ -1207,17 +1266,23 @@ function firstChoice(completion) {
1207
1266
  const choices = Array.isArray(completion.choices) ? completion.choices : [];
1208
1267
  return asRecord(choices[0]);
1209
1268
  }
1210
- function processCompletionSseLine(line, enqueue, markDone) {
1211
- const trimmed = line.trim();
1212
- if (!trimmed.startsWith("data:")) {
1213
- return;
1269
+ function processCompletionSseBlock(block, enqueue, markTerminal) {
1270
+ let event = "message";
1271
+ const dataLines = [];
1272
+ for (const line of block.split(/\r?\n/)) {
1273
+ const trimmed = line.trim();
1274
+ if (trimmed.startsWith("event:")) {
1275
+ event = trimmed.slice("event:".length).trim() || event;
1276
+ } else if (trimmed.startsWith("data:")) {
1277
+ dataLines.push(trimmed.slice("data:".length).trim());
1278
+ }
1214
1279
  }
1215
- const data = trimmed.slice("data:".length).trim();
1280
+ const data = dataLines.join("\n");
1216
1281
  if (!data) {
1217
1282
  return;
1218
1283
  }
1219
1284
  if (data === "[DONE]") {
1220
- markDone();
1285
+ markTerminal();
1221
1286
  enqueue("[DONE]");
1222
1287
  return;
1223
1288
  }
@@ -1225,6 +1290,12 @@ function processCompletionSseLine(line, enqueue, markDone) {
1225
1290
  if (!parsed) {
1226
1291
  return;
1227
1292
  }
1293
+ const error = completionStreamError(event, parsed);
1294
+ if (error) {
1295
+ markTerminal();
1296
+ enqueue({ error });
1297
+ return;
1298
+ }
1228
1299
  const choice = firstChoice(parsed);
1229
1300
  const delta = asRecord(choice.delta);
1230
1301
  const text = contentToText(delta.content);
@@ -1252,6 +1323,22 @@ function processCompletionSseLine(line, enqueue, markDone) {
1252
1323
  })
1253
1324
  );
1254
1325
  }
1326
+ function completionStreamError(event, parsed) {
1327
+ const responseError = asRecord(asRecord(parsed.response).error);
1328
+ const directError = asRecord(parsed.error);
1329
+ const error = Object.keys(directError).length > 0 ? directError : Object.keys(responseError).length > 0 ? responseError : void 0;
1330
+ if (error) {
1331
+ return error;
1332
+ }
1333
+ if (event === "error" || parsed.type === "response.failed") {
1334
+ return removeUndefined({
1335
+ code: contentToText(parsed.code) || void 0,
1336
+ message: contentToText(parsed.message) || "Upstream streaming request failed.",
1337
+ type: contentToText(parsed.type) || "upstream_stream_error"
1338
+ });
1339
+ }
1340
+ return void 0;
1341
+ }
1255
1342
  function processChatSseLine(line, handlers) {
1256
1343
  const trimmed = line.trim();
1257
1344
  if (!trimmed.startsWith("data:")) {
@@ -1701,10 +1788,19 @@ var DEFAULT_HOST = "127.0.0.1";
1701
1788
  var DEFAULT_PORT = 4141;
1702
1789
  var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
1703
1790
  var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
1791
+ var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
1792
+ var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
1793
+ var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
1704
1794
  var USAGE_CACHE_TTL_MS = 6e4;
1795
+ var RequestBodyTooLargeError = class extends Error {
1796
+ constructor() {
1797
+ super(REQUEST_TOO_LARGE_MESSAGE);
1798
+ this.name = "RequestBodyTooLargeError";
1799
+ }
1800
+ };
1705
1801
  function createHoopilotHandler(options = {}) {
1706
1802
  const client = new CopilotClient(options);
1707
- const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
1803
+ const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
1708
1804
  const logger = serverLogger(options);
1709
1805
  const metrics = options.metrics ?? new MetricsRegistry();
1710
1806
  const readUsage = createUsageReader(client, metrics);
@@ -1790,6 +1886,12 @@ function createHoopilotHandler(options = {}) {
1790
1886
  "request body was invalid json"
1791
1887
  );
1792
1888
  return finish(jsonError(400, "invalid_request_error", message));
1889
+ } else if (error instanceof RequestBodyTooLargeError) {
1890
+ requestLogger.warn(
1891
+ { err: errorDetails(error), event: "http.request.failed" },
1892
+ "request body exceeded size limit"
1893
+ );
1894
+ return finish(jsonError(413, "request_too_large", message));
1793
1895
  } else {
1794
1896
  requestLogger.error(
1795
1897
  { err: errorDetails(error), event: "http.request.failed" },
@@ -1801,10 +1903,10 @@ function createHoopilotHandler(options = {}) {
1801
1903
  };
1802
1904
  }
1803
1905
  function startHoopilotServer(options = {}) {
1804
- const host = options.host ?? options.env?.HOST ?? DEFAULT_HOST;
1805
- const port = normalizeServerPort(options.port ?? options.env?.PORT ?? DEFAULT_PORT);
1806
- const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
1807
- const allowUnauthenticated = options.allowUnauthenticated ?? options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED === "1";
1906
+ const host = options.host ?? envValue(options.env?.HOST) ?? DEFAULT_HOST;
1907
+ const port = normalizeServerPort(options.port ?? envValue(options.env?.PORT) ?? DEFAULT_PORT);
1908
+ const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
1909
+ const allowUnauthenticated = options.allowUnauthenticated ?? envValue(options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED) === "1";
1808
1910
  if (!isLoopbackHost(host) && !apiKey && !allowUnauthenticated) {
1809
1911
  throw new Error(
1810
1912
  "Refusing to listen on a non-loopback host without HOOPILOT_API_KEY. Set an API key or pass --allow-unauthenticated."
@@ -1822,7 +1924,7 @@ function startHoopilotServer(options = {}) {
1822
1924
  });
1823
1925
  return {
1824
1926
  server,
1825
- url: `http://${host}:${server.port}`
1927
+ url: `http://${urlHost(host)}:${server.port}`
1826
1928
  };
1827
1929
  }
1828
1930
  async function handleModels(client, metrics, signal, logger) {
@@ -1931,14 +2033,15 @@ function proxyResponse(upstream) {
1931
2033
  });
1932
2034
  }
1933
2035
  async function readJson(request) {
2036
+ const text = await readRequestText(request);
1934
2037
  try {
1935
- return asRecord(await request.json());
2038
+ return asRecord(JSON.parse(text));
1936
2039
  } catch {
1937
2040
  throw new Error(INVALID_JSON_MESSAGE);
1938
2041
  }
1939
2042
  }
1940
2043
  async function readJsonText(request) {
1941
- const text = await request.text();
2044
+ const text = await readRequestText(request);
1942
2045
  try {
1943
2046
  JSON.parse(text);
1944
2047
  return text;
@@ -1946,6 +2049,40 @@ async function readJsonText(request) {
1946
2049
  throw new Error(INVALID_JSON_MESSAGE);
1947
2050
  }
1948
2051
  }
2052
+ async function readRequestText(request) {
2053
+ const contentLength = request.headers.get("content-length");
2054
+ if (contentLength) {
2055
+ const declaredBytes = Number(contentLength);
2056
+ if (Number.isFinite(declaredBytes) && declaredBytes > MAX_REQUEST_BODY_BYTES) {
2057
+ throw new RequestBodyTooLargeError();
2058
+ }
2059
+ }
2060
+ const body = request.body;
2061
+ if (!body) {
2062
+ return "";
2063
+ }
2064
+ const reader = body.getReader();
2065
+ const decoder = new TextDecoder();
2066
+ let bytes = 0;
2067
+ let text = "";
2068
+ try {
2069
+ while (true) {
2070
+ const { done, value } = await reader.read();
2071
+ if (done) {
2072
+ return `${text}${decoder.decode()}`;
2073
+ }
2074
+ bytes += value.byteLength;
2075
+ if (bytes > MAX_REQUEST_BODY_BYTES) {
2076
+ await reader.cancel().catch(() => {
2077
+ });
2078
+ throw new RequestBodyTooLargeError();
2079
+ }
2080
+ text += decoder.decode(value, { stream: true });
2081
+ }
2082
+ } finally {
2083
+ reader.releaseLock();
2084
+ }
2085
+ }
1949
2086
  function jsonResponse(body, status = 200) {
1950
2087
  return new Response(JSON.stringify(body), {
1951
2088
  headers: {
@@ -2018,6 +2155,9 @@ function upstreamAuthMessage(message) {
2018
2155
  function isLoopbackHost(host) {
2019
2156
  return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
2020
2157
  }
2158
+ function urlHost(host) {
2159
+ return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
2160
+ }
2021
2161
  function isLoopbackOrigin(origin) {
2022
2162
  try {
2023
2163
  return isLoopbackHost(new URL(origin).hostname.toLowerCase());
@@ -2125,7 +2265,7 @@ function logRequestCompleted(logger, status, stream, durationMs) {
2125
2265
  }
2126
2266
  function requestIdFor(request) {
2127
2267
  const existing = request.headers.get("x-request-id")?.trim();
2128
- return existing || crypto.randomUUID();
2268
+ return existing && REQUEST_ID_PATTERN.test(existing) ? existing : crypto.randomUUID();
2129
2269
  }
2130
2270
  function canonicalApiPath(path) {
2131
2271
  const withoutTrailingSlash = path.length > 1 ? path.replace(/\/+$/, "") : path;