@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/index.js CHANGED
@@ -1,6 +1,56 @@
1
1
  // src/auth-store.ts
2
2
  import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
3
3
  import { dirname, join } from "path";
4
+
5
+ // src/util.ts
6
+ function trimTrailingSlash(value) {
7
+ return value.replace(/\/+$/, "");
8
+ }
9
+ function envValue(value) {
10
+ const trimmed = value?.trim();
11
+ return trimmed ? trimmed : void 0;
12
+ }
13
+ function isTrustedTokenBaseUrl(rawUrl, allowedHttpsHosts, allowUnsafeHttps = false) {
14
+ const url = parseUrl(rawUrl);
15
+ if (!url) {
16
+ return false;
17
+ }
18
+ if (url.username || url.password || url.search || url.hash) {
19
+ return false;
20
+ }
21
+ if (url.pathname !== "" && url.pathname !== "/") {
22
+ return false;
23
+ }
24
+ if (isLoopbackHttpUrl(url)) {
25
+ return true;
26
+ }
27
+ if (url.protocol !== "https:") {
28
+ return false;
29
+ }
30
+ const host = url.hostname.toLowerCase();
31
+ return allowedHttpsHosts.includes(host) || allowUnsafeHttps;
32
+ }
33
+ function parseUrl(rawUrl) {
34
+ let url;
35
+ try {
36
+ url = new URL(rawUrl);
37
+ } catch {
38
+ return void 0;
39
+ }
40
+ return url;
41
+ }
42
+ function isLoopbackHttpUrl(url) {
43
+ return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1" || url.hostname === "[::1]");
44
+ }
45
+ async function truncatedResponseText(response, max = 500) {
46
+ const text = await response.text();
47
+ return text.slice(0, max);
48
+ }
49
+ function asRecord(value) {
50
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
51
+ }
52
+
53
+ // src/auth-store.ts
4
54
  var StoredCopilotAuthError = class extends Error {
5
55
  constructor(message) {
6
56
  super(message);
@@ -8,10 +58,25 @@ var StoredCopilotAuthError = class extends Error {
8
58
  }
9
59
  };
10
60
  function authStorePath(env = process.env) {
11
- if (env.HOOPILOT_AUTH_FILE) {
12
- return env.HOOPILOT_AUTH_FILE;
61
+ const explicit = envValue(env.HOOPILOT_AUTH_FILE);
62
+ if (explicit) {
63
+ return explicit;
64
+ }
65
+ const xdg = envValue(env.XDG_CONFIG_HOME);
66
+ if (xdg) {
67
+ return join(xdg, "hoopilot", "auth.json");
68
+ }
69
+ const appdata = envValue(env.APPDATA);
70
+ if (appdata) {
71
+ return join(appdata, "hoopilot", "auth.json");
72
+ }
73
+ const home = envValue(env.HOME);
74
+ if (!home) {
75
+ throw new StoredCopilotAuthError(
76
+ "Cannot resolve Hoopilot auth file path without HOOPILOT_AUTH_FILE, XDG_CONFIG_HOME, APPDATA, or HOME."
77
+ );
13
78
  }
14
- const base = env.XDG_CONFIG_HOME ?? env.APPDATA ?? (env.HOME ? join(env.HOME, ".config") : join(process.cwd(), ".config"));
79
+ const base = join(home, ".config");
15
80
  return join(base, "hoopilot", "auth.json");
16
81
  }
17
82
  function readStoredCopilotAuth(path = authStorePath()) {
@@ -68,30 +133,6 @@ function writeStoredCopilotAuth(auth, path = authStorePath()) {
68
133
  }
69
134
  }
70
135
 
71
- // src/util.ts
72
- function trimTrailingSlash(value) {
73
- return value.replace(/\/+$/, "");
74
- }
75
- function isHttpsOrLoopbackUrl(rawUrl) {
76
- let url;
77
- try {
78
- url = new URL(rawUrl);
79
- } catch {
80
- return false;
81
- }
82
- if (url.protocol === "https:") {
83
- return true;
84
- }
85
- return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1" || url.hostname === "[::1]");
86
- }
87
- async function truncatedResponseText(response, max = 500) {
88
- const text = await response.text();
89
- return text.slice(0, max);
90
- }
91
- function asRecord(value) {
92
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
93
- }
94
-
95
136
  // src/auth.ts
96
137
  var DEFAULT_COPILOT_API_BASE_URL = "https://api.githubcopilot.com";
97
138
  var REFRESH_SKEW_MS = 6e4;
@@ -105,11 +146,15 @@ var CopilotAuthError = class extends Error {
105
146
  var CopilotAuth = class {
106
147
  #authStorePath;
107
148
  #copilotApiBaseUrl;
149
+ #hasCopilotApiBaseUrlOverride;
108
150
  #cachedAccess;
109
151
  constructor(options = {}) {
110
- this.#authStorePath = options.authStorePath ?? options.env?.HOOPILOT_AUTH_FILE;
152
+ const envAuthStorePath = envValue(options.env?.HOOPILOT_AUTH_FILE);
153
+ const envCopilotApiBaseUrl = envValue(options.env?.COPILOT_API_BASE_URL);
154
+ this.#authStorePath = options.authStorePath ?? envAuthStorePath;
155
+ this.#hasCopilotApiBaseUrlOverride = Boolean(options.copilotApiBaseUrl ?? envCopilotApiBaseUrl);
111
156
  this.#copilotApiBaseUrl = trimTrailingSlash(
112
- options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL
157
+ options.copilotApiBaseUrl ?? envCopilotApiBaseUrl ?? DEFAULT_COPILOT_API_BASE_URL
113
158
  );
114
159
  }
115
160
  async getAccess() {
@@ -127,7 +172,9 @@ var CopilotAuth = class {
127
172
  }
128
173
  if (stored) {
129
174
  return this.#cacheAccess({
130
- apiBaseUrl: trimTrailingSlash(stored.apiBaseUrl ?? this.#copilotApiBaseUrl),
175
+ apiBaseUrl: trimTrailingSlash(
176
+ this.#hasCopilotApiBaseUrlOverride ? this.#copilotApiBaseUrl : stored.apiBaseUrl ?? this.#copilotApiBaseUrl
177
+ ),
131
178
  expiresAtMs: Date.now() + STORED_TOKEN_TTL_MS,
132
179
  source: "github-copilot-oauth",
133
180
  token: stored.token
@@ -145,6 +192,8 @@ var CopilotAuth = class {
145
192
 
146
193
  // src/copilot.ts
147
194
  var DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
195
+ var ALLOWED_COPILOT_API_HOSTS = ["api.githubcopilot.com"];
196
+ var ALLOWED_GITHUB_API_HOSTS = ["api.github.com"];
148
197
  var COPILOT_USAGE_API_VERSION = "2025-04-01";
149
198
  function applyCopilotHeaders(headers, token) {
150
199
  headers.set("accept", headers.get("accept") ?? "application/json");
@@ -168,13 +217,15 @@ function applyGithubApiHeaders(headers, token) {
168
217
  }
169
218
  var CopilotClient = class {
170
219
  #auth;
220
+ #allowUnsafeUpstream;
171
221
  #fetch;
172
222
  #githubApiBaseUrl;
173
223
  constructor(options = {}) {
174
224
  this.#auth = new CopilotAuth(options);
225
+ this.#allowUnsafeUpstream = envValue(options.env?.HOOPILOT_ALLOW_UNSAFE_UPSTREAM) === "1";
175
226
  this.#fetch = options.fetch ?? fetch;
176
227
  this.#githubApiBaseUrl = trimTrailingSlash(
177
- options.githubApiBaseUrl ?? options.env?.HOOPILOT_GITHUB_API_BASE_URL ?? DEFAULT_GITHUB_API_BASE_URL
228
+ options.githubApiBaseUrl ?? envValue(options.env?.HOOPILOT_GITHUB_API_BASE_URL) ?? DEFAULT_GITHUB_API_BASE_URL
178
229
  );
179
230
  }
180
231
  /**
@@ -183,9 +234,13 @@ var CopilotClient = class {
183
234
  * accepted directly here — no Copilot token exchange is required to read quota.
184
235
  */
185
236
  async usage(signal) {
186
- if (!isHttpsOrLoopbackUrl(this.#githubApiBaseUrl)) {
237
+ if (!isTrustedTokenBaseUrl(
238
+ this.#githubApiBaseUrl,
239
+ ALLOWED_GITHUB_API_HOSTS,
240
+ this.#allowUnsafeUpstream
241
+ )) {
187
242
  throw new Error(
188
- `Refusing to send the GitHub OAuth token to a non-HTTPS host: ${this.#githubApiBaseUrl}`
243
+ `Refusing to send the GitHub OAuth token to an untrusted GitHub API host: ${this.#githubApiBaseUrl}`
189
244
  );
190
245
  }
191
246
  const access = await this.#auth.getAccess();
@@ -227,9 +282,13 @@ var CopilotClient = class {
227
282
  }
228
283
  async fetchCopilot(path, init) {
229
284
  const access = await this.#auth.getAccess();
230
- if (!isHttpsOrLoopbackUrl(access.apiBaseUrl)) {
285
+ if (!isTrustedTokenBaseUrl(
286
+ access.apiBaseUrl,
287
+ ALLOWED_COPILOT_API_HOSTS,
288
+ this.#allowUnsafeUpstream
289
+ )) {
231
290
  throw new Error(
232
- `Refusing to send the GitHub OAuth token to a non-HTTPS host: ${access.apiBaseUrl}`
291
+ `Refusing to send the GitHub OAuth token to an untrusted Copilot API host: ${access.apiBaseUrl}`
233
292
  );
234
293
  }
235
294
  const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
@@ -315,9 +374,9 @@ async function githubCopilotDeviceLogin(options = {}) {
315
374
  const fetcher = options.fetch ?? fetch;
316
375
  const sleeper = options.sleep ?? sleep;
317
376
  const domain = normalizeDomain(
318
- options.domain ?? env.HOOPILOT_GITHUB_DOMAIN ?? DEFAULT_GITHUB_DOMAIN
377
+ options.domain ?? envValue(env.HOOPILOT_GITHUB_DOMAIN) ?? DEFAULT_GITHUB_DOMAIN
319
378
  );
320
- const clientId = options.clientId ?? env.HOOPILOT_GITHUB_CLIENT_ID ?? env.COPILOT_GITHUB_CLIENT_ID ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
379
+ const clientId = options.clientId ?? envValue(env.HOOPILOT_GITHUB_CLIENT_ID) ?? envValue(env.COPILOT_GITHUB_CLIENT_ID) ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
321
380
  const device = await requestDeviceCode(fetcher, domain, clientId);
322
381
  const verificationUrl = device.verification_uri;
323
382
  const userCode = device.user_code;
@@ -475,8 +534,8 @@ var noopLogger = {
475
534
  };
476
535
  function createHoopilotLogger(options = {}) {
477
536
  const env = options.env ?? process.env;
478
- const level = parseLogLevel(options.level ?? env.HOOPILOT_LOG_LEVEL);
479
- const format = parseLogFormat(options.format ?? env.HOOPILOT_LOG_FORMAT);
537
+ const level = parseLogLevel(options.level ?? envValue(env.HOOPILOT_LOG_LEVEL));
538
+ const format = parseLogFormat(options.format ?? envValue(env.HOOPILOT_LOG_FORMAT));
480
539
  const pinoOptions = {
481
540
  base: {
482
541
  service: "hoopilot",
@@ -526,7 +585,7 @@ function parseLogLevel(value) {
526
585
  }
527
586
  function shouldCreateLogger(options) {
528
587
  return Boolean(
529
- options.logger || options.logFormat || options.logLevel || options.env?.HOOPILOT_LOG_FORMAT || options.env?.HOOPILOT_LOG_LEVEL
588
+ options.logger || options.logFormat || options.logLevel || envValue(options.env?.HOOPILOT_LOG_FORMAT) || envValue(options.env?.HOOPILOT_LOG_LEVEL)
530
589
  );
531
590
  }
532
591
  function errorDetails(error) {
@@ -645,12 +704,15 @@ function completionStreamFromChatStream(chatStream) {
645
704
  const encoder = new TextEncoder();
646
705
  const decoder = new TextDecoder();
647
706
  let buffer = "";
648
- let sawDone = false;
707
+ let sawTerminalEvent = false;
649
708
  return new ReadableStream({
650
709
  async start(controller) {
651
710
  const enqueue = (data) => {
652
711
  controller.enqueue(encoder.encode(encodeDataSse(data)));
653
712
  };
713
+ const markTerminal = () => {
714
+ sawTerminalEvent = true;
715
+ };
654
716
  const reader = chatStream.getReader();
655
717
  try {
656
718
  while (true) {
@@ -659,20 +721,17 @@ function completionStreamFromChatStream(chatStream) {
659
721
  break;
660
722
  }
661
723
  buffer += decoder.decode(result.value, { stream: true });
662
- const lines = buffer.split(/\r?\n/);
663
- buffer = lines.pop() ?? "";
664
- for (const line of lines) {
665
- processCompletionSseLine(line, enqueue, () => {
666
- sawDone = true;
667
- });
724
+ const blocks = buffer.split(/\r?\n\r?\n/);
725
+ buffer = blocks.pop() ?? "";
726
+ for (const block of blocks) {
727
+ processCompletionSseBlock(block, enqueue, markTerminal);
668
728
  }
669
729
  }
670
- if (buffer) {
671
- processCompletionSseLine(buffer, enqueue, () => {
672
- sawDone = true;
673
- });
730
+ const tail = `${buffer}${decoder.decode()}`;
731
+ if (tail.trim()) {
732
+ processCompletionSseBlock(tail, enqueue, markTerminal);
674
733
  }
675
- if (!sawDone) {
734
+ if (!sawTerminalEvent) {
676
735
  enqueue("[DONE]");
677
736
  }
678
737
  controller.close();
@@ -1137,17 +1196,23 @@ function firstChoice(completion) {
1137
1196
  const choices = Array.isArray(completion.choices) ? completion.choices : [];
1138
1197
  return asRecord(choices[0]);
1139
1198
  }
1140
- function processCompletionSseLine(line, enqueue, markDone) {
1141
- const trimmed = line.trim();
1142
- if (!trimmed.startsWith("data:")) {
1143
- return;
1199
+ function processCompletionSseBlock(block, enqueue, markTerminal) {
1200
+ let event = "message";
1201
+ const dataLines = [];
1202
+ for (const line of block.split(/\r?\n/)) {
1203
+ const trimmed = line.trim();
1204
+ if (trimmed.startsWith("event:")) {
1205
+ event = trimmed.slice("event:".length).trim() || event;
1206
+ } else if (trimmed.startsWith("data:")) {
1207
+ dataLines.push(trimmed.slice("data:".length).trim());
1208
+ }
1144
1209
  }
1145
- const data = trimmed.slice("data:".length).trim();
1210
+ const data = dataLines.join("\n");
1146
1211
  if (!data) {
1147
1212
  return;
1148
1213
  }
1149
1214
  if (data === "[DONE]") {
1150
- markDone();
1215
+ markTerminal();
1151
1216
  enqueue("[DONE]");
1152
1217
  return;
1153
1218
  }
@@ -1155,6 +1220,12 @@ function processCompletionSseLine(line, enqueue, markDone) {
1155
1220
  if (!parsed) {
1156
1221
  return;
1157
1222
  }
1223
+ const error = completionStreamError(event, parsed);
1224
+ if (error) {
1225
+ markTerminal();
1226
+ enqueue({ error });
1227
+ return;
1228
+ }
1158
1229
  const choice = firstChoice(parsed);
1159
1230
  const delta = asRecord(choice.delta);
1160
1231
  const text = contentToText(delta.content);
@@ -1182,6 +1253,22 @@ function processCompletionSseLine(line, enqueue, markDone) {
1182
1253
  })
1183
1254
  );
1184
1255
  }
1256
+ function completionStreamError(event, parsed) {
1257
+ const responseError = asRecord(asRecord(parsed.response).error);
1258
+ const directError = asRecord(parsed.error);
1259
+ const error = Object.keys(directError).length > 0 ? directError : Object.keys(responseError).length > 0 ? responseError : void 0;
1260
+ if (error) {
1261
+ return error;
1262
+ }
1263
+ if (event === "error" || parsed.type === "response.failed") {
1264
+ return removeUndefined({
1265
+ code: contentToText(parsed.code) || void 0,
1266
+ message: contentToText(parsed.message) || "Upstream streaming request failed.",
1267
+ type: contentToText(parsed.type) || "upstream_stream_error"
1268
+ });
1269
+ }
1270
+ return void 0;
1271
+ }
1185
1272
  function processChatSseLine(line, handlers) {
1186
1273
  const trimmed = line.trim();
1187
1274
  if (!trimmed.startsWith("data:")) {
@@ -1631,10 +1718,19 @@ var DEFAULT_HOST = "127.0.0.1";
1631
1718
  var DEFAULT_PORT = 4141;
1632
1719
  var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
1633
1720
  var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
1721
+ var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
1722
+ var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
1723
+ var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
1634
1724
  var USAGE_CACHE_TTL_MS = 6e4;
1725
+ var RequestBodyTooLargeError = class extends Error {
1726
+ constructor() {
1727
+ super(REQUEST_TOO_LARGE_MESSAGE);
1728
+ this.name = "RequestBodyTooLargeError";
1729
+ }
1730
+ };
1635
1731
  function createHoopilotHandler(options = {}) {
1636
1732
  const client = new CopilotClient(options);
1637
- const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
1733
+ const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
1638
1734
  const logger = serverLogger(options);
1639
1735
  const metrics = options.metrics ?? new MetricsRegistry();
1640
1736
  const readUsage = createUsageReader(client, metrics);
@@ -1720,6 +1816,12 @@ function createHoopilotHandler(options = {}) {
1720
1816
  "request body was invalid json"
1721
1817
  );
1722
1818
  return finish(jsonError(400, "invalid_request_error", message));
1819
+ } else if (error instanceof RequestBodyTooLargeError) {
1820
+ requestLogger.warn(
1821
+ { err: errorDetails(error), event: "http.request.failed" },
1822
+ "request body exceeded size limit"
1823
+ );
1824
+ return finish(jsonError(413, "request_too_large", message));
1723
1825
  } else {
1724
1826
  requestLogger.error(
1725
1827
  { err: errorDetails(error), event: "http.request.failed" },
@@ -1731,10 +1833,10 @@ function createHoopilotHandler(options = {}) {
1731
1833
  };
1732
1834
  }
1733
1835
  function startHoopilotServer(options = {}) {
1734
- const host = options.host ?? options.env?.HOST ?? DEFAULT_HOST;
1735
- const port = normalizeServerPort(options.port ?? options.env?.PORT ?? DEFAULT_PORT);
1736
- const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
1737
- const allowUnauthenticated = options.allowUnauthenticated ?? options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED === "1";
1836
+ const host = options.host ?? envValue(options.env?.HOST) ?? DEFAULT_HOST;
1837
+ const port = normalizeServerPort(options.port ?? envValue(options.env?.PORT) ?? DEFAULT_PORT);
1838
+ const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
1839
+ const allowUnauthenticated = options.allowUnauthenticated ?? envValue(options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED) === "1";
1738
1840
  if (!isLoopbackHost(host) && !apiKey && !allowUnauthenticated) {
1739
1841
  throw new Error(
1740
1842
  "Refusing to listen on a non-loopback host without HOOPILOT_API_KEY. Set an API key or pass --allow-unauthenticated."
@@ -1752,7 +1854,7 @@ function startHoopilotServer(options = {}) {
1752
1854
  });
1753
1855
  return {
1754
1856
  server,
1755
- url: `http://${host}:${server.port}`
1857
+ url: `http://${urlHost(host)}:${server.port}`
1756
1858
  };
1757
1859
  }
1758
1860
  async function handleModels(client, metrics, signal, logger) {
@@ -1861,14 +1963,15 @@ function proxyResponse(upstream) {
1861
1963
  });
1862
1964
  }
1863
1965
  async function readJson(request) {
1966
+ const text = await readRequestText(request);
1864
1967
  try {
1865
- return asRecord(await request.json());
1968
+ return asRecord(JSON.parse(text));
1866
1969
  } catch {
1867
1970
  throw new Error(INVALID_JSON_MESSAGE);
1868
1971
  }
1869
1972
  }
1870
1973
  async function readJsonText(request) {
1871
- const text = await request.text();
1974
+ const text = await readRequestText(request);
1872
1975
  try {
1873
1976
  JSON.parse(text);
1874
1977
  return text;
@@ -1876,6 +1979,40 @@ async function readJsonText(request) {
1876
1979
  throw new Error(INVALID_JSON_MESSAGE);
1877
1980
  }
1878
1981
  }
1982
+ async function readRequestText(request) {
1983
+ const contentLength = request.headers.get("content-length");
1984
+ if (contentLength) {
1985
+ const declaredBytes = Number(contentLength);
1986
+ if (Number.isFinite(declaredBytes) && declaredBytes > MAX_REQUEST_BODY_BYTES) {
1987
+ throw new RequestBodyTooLargeError();
1988
+ }
1989
+ }
1990
+ const body = request.body;
1991
+ if (!body) {
1992
+ return "";
1993
+ }
1994
+ const reader = body.getReader();
1995
+ const decoder = new TextDecoder();
1996
+ let bytes = 0;
1997
+ let text = "";
1998
+ try {
1999
+ while (true) {
2000
+ const { done, value } = await reader.read();
2001
+ if (done) {
2002
+ return `${text}${decoder.decode()}`;
2003
+ }
2004
+ bytes += value.byteLength;
2005
+ if (bytes > MAX_REQUEST_BODY_BYTES) {
2006
+ await reader.cancel().catch(() => {
2007
+ });
2008
+ throw new RequestBodyTooLargeError();
2009
+ }
2010
+ text += decoder.decode(value, { stream: true });
2011
+ }
2012
+ } finally {
2013
+ reader.releaseLock();
2014
+ }
2015
+ }
1879
2016
  function jsonResponse(body, status = 200) {
1880
2017
  return new Response(JSON.stringify(body), {
1881
2018
  headers: {
@@ -1948,6 +2085,9 @@ function upstreamAuthMessage(message) {
1948
2085
  function isLoopbackHost(host) {
1949
2086
  return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
1950
2087
  }
2088
+ function urlHost(host) {
2089
+ return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
2090
+ }
1951
2091
  function isLoopbackOrigin(origin) {
1952
2092
  try {
1953
2093
  return isLoopbackHost(new URL(origin).hostname.toLowerCase());
@@ -2055,7 +2195,7 @@ function logRequestCompleted(logger, status, stream, durationMs) {
2055
2195
  }
2056
2196
  function requestIdFor(request) {
2057
2197
  const existing = request.headers.get("x-request-id")?.trim();
2058
- return existing || crypto.randomUUID();
2198
+ return existing && REQUEST_ID_PATTERN.test(existing) ? existing : crypto.randomUUID();
2059
2199
  }
2060
2200
  function canonicalApiPath(path) {
2061
2201
  const withoutTrailingSlash = path.length > 1 ? path.replace(/\/+$/, "") : path;