@openhoo/hoopilot 0.9.3 → 1.0.0

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-7GSQVYYT.js";
6
+ } from "./chunk-JU6F5L34.js";
7
7
  export {
8
8
  buildCodexxInvocation,
9
9
  main,
package/dist/index.cjs CHANGED
@@ -2694,7 +2694,8 @@ var IS_STANDALONE_BINARY = BAKED_VERSION !== void 0;
2694
2694
  // src/server.ts
2695
2695
  var DEFAULT_HOST = "127.0.0.1";
2696
2696
  var DEFAULT_PORT = 4141;
2697
- var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
2697
+ var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Cross-origin browser requests are blocked unless the Origin is loopback or listed in HOOPILOT_ALLOWED_ORIGINS.";
2698
+ var WELL_KNOWN_DEMO_API_KEYS = /* @__PURE__ */ new Set(["local-key"]);
2698
2699
  var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
2699
2700
  var JSON_OBJECT_MESSAGE = "Request body must be a JSON object.";
2700
2701
  var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
@@ -2710,6 +2711,7 @@ var RequestBodyTooLargeError = class extends Error {
2710
2711
  function createHoopilotHandler(options = {}) {
2711
2712
  const client = new CopilotClient(options);
2712
2713
  const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
2714
+ const allowedOrigins = parseAllowedOrigins(options.env);
2713
2715
  const logger = serverLogger(options);
2714
2716
  const metrics = options.metrics ?? new MetricsRegistry();
2715
2717
  const readUsage = createUsageReader(client, metrics);
@@ -2729,7 +2731,10 @@ function createHoopilotHandler(options = {}) {
2729
2731
  route
2730
2732
  });
2731
2733
  metrics.startRequest();
2734
+ const origin = request.headers.get("origin")?.trim() || void 0;
2735
+ const corsOrigin = resolveCorsAllowOrigin(origin, allowedOrigins);
2732
2736
  const finish = (response) => finishResponse(response, {
2737
+ corsOrigin,
2733
2738
  logger: requestLogger,
2734
2739
  method: request.method,
2735
2740
  metrics,
@@ -2739,11 +2744,11 @@ function createHoopilotHandler(options = {}) {
2739
2744
  closeConnection: bufferProxyBodies,
2740
2745
  trackStreamingBody: !bufferProxyBodies
2741
2746
  });
2742
- const browserOrigin = forbiddenBrowserOrigin(request, apiKey);
2747
+ const browserOrigin = forbiddenBrowserOrigin(origin, request, allowedOrigins);
2743
2748
  if (browserOrigin) {
2744
2749
  requestLogger.warn(
2745
2750
  { event: "http.request.forbidden_origin", origin: browserOrigin },
2746
- "blocked unauthenticated browser-origin request"
2751
+ "blocked cross-origin browser request"
2747
2752
  );
2748
2753
  return finish(jsonError(403, "forbidden_origin", FORBIDDEN_BROWSER_ORIGIN_MESSAGE));
2749
2754
  }
@@ -2869,10 +2874,17 @@ function startHoopilotServer(options = {}) {
2869
2874
  const port = normalizeServerPort(options.port ?? envValue(options.env?.PORT) ?? DEFAULT_PORT);
2870
2875
  const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
2871
2876
  const allowUnauthenticated = options.allowUnauthenticated ?? envValue(options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED) === "1";
2872
- if (!isLoopbackHost(host) && !apiKey && !allowUnauthenticated) {
2873
- throw new Error(
2874
- "Refusing to listen on a non-loopback host without HOOPILOT_API_KEY. Set an API key or pass --allow-unauthenticated."
2875
- );
2877
+ if (!isLoopbackHost(host)) {
2878
+ if (!apiKey && !allowUnauthenticated) {
2879
+ throw new Error(
2880
+ "Refusing to listen on a non-loopback host without HOOPILOT_API_KEY. Set an API key or pass --allow-unauthenticated."
2881
+ );
2882
+ }
2883
+ if (apiKey && isWellKnownDemoApiKey(apiKey)) {
2884
+ throw new Error(
2885
+ "Refusing to listen on a non-loopback host with a well-known demo HOOPILOT_API_KEY. Set a strong, unique API key."
2886
+ );
2887
+ }
2876
2888
  }
2877
2889
  const server = Bun.serve({
2878
2890
  fetch: createHoopilotHandler({
@@ -3185,7 +3197,6 @@ function corsHeaders() {
3185
3197
  return {
3186
3198
  "access-control-allow-headers": "anthropic-beta, anthropic-dangerous-direct-browser-access, anthropic-version, authorization, content-type, x-api-key, x-request-id",
3187
3199
  "access-control-allow-methods": "GET, POST, OPTIONS",
3188
- "access-control-allow-origin": "*",
3189
3200
  "access-control-expose-headers": "x-request-id"
3190
3201
  };
3191
3202
  }
@@ -3197,17 +3208,34 @@ function isAuthorized(request, apiKey) {
3197
3208
  const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
3198
3209
  return bearer === apiKey || request.headers.get("x-api-key") === apiKey;
3199
3210
  }
3200
- function forbiddenBrowserOrigin(request, apiKey) {
3201
- if (apiKey) {
3202
- return void 0;
3203
- }
3204
- const origin = request.headers.get("origin")?.trim();
3211
+ function forbiddenBrowserOrigin(origin, request, allowedOrigins) {
3205
3212
  if (origin) {
3206
- return isLoopbackOrigin(origin) ? void 0 : origin;
3213
+ return isAllowedOrigin(origin, allowedOrigins) ? void 0 : origin;
3207
3214
  }
3208
3215
  const fetchSite = request.headers.get("sec-fetch-site")?.toLowerCase();
3209
3216
  return fetchSite === "cross-site" ? "cross-site" : void 0;
3210
3217
  }
3218
+ function parseAllowedOrigins(env) {
3219
+ const raw = envValue(env?.HOOPILOT_ALLOWED_ORIGINS);
3220
+ if (!raw) {
3221
+ return /* @__PURE__ */ new Set();
3222
+ }
3223
+ return new Set(
3224
+ raw.split(",").map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0)
3225
+ );
3226
+ }
3227
+ function isAllowedOrigin(origin, allowedOrigins) {
3228
+ return isLoopbackOrigin(origin) || allowedOrigins.has(origin.toLowerCase());
3229
+ }
3230
+ function resolveCorsAllowOrigin(origin, allowedOrigins) {
3231
+ if (!origin) {
3232
+ return "*";
3233
+ }
3234
+ return isAllowedOrigin(origin, allowedOrigins) ? origin : void 0;
3235
+ }
3236
+ function isWellKnownDemoApiKey(apiKey) {
3237
+ return WELL_KNOWN_DEMO_API_KEYS.has(apiKey.trim().toLowerCase());
3238
+ }
3211
3239
  function isUpstreamAuthStatus(status) {
3212
3240
  return status === 401 || status === 403;
3213
3241
  }
@@ -3267,7 +3295,12 @@ function shouldBufferProxyBodies(mode) {
3267
3295
  return process.platform === "win32" && IS_STANDALONE_BINARY;
3268
3296
  }
3269
3297
  function finishResponse(response, options) {
3270
- const withRequestId = responseWithRequestId(response, options.requestId, options.closeConnection);
3298
+ const withRequestId = responseWithRequestId(
3299
+ response,
3300
+ options.requestId,
3301
+ options.closeConnection,
3302
+ options.corsOrigin
3303
+ );
3271
3304
  const stream = isStreamingResponse(withRequestId);
3272
3305
  const status = withRequestId.status;
3273
3306
  const complete = () => {
@@ -3285,9 +3318,17 @@ function finishResponse(response, options) {
3285
3318
  complete();
3286
3319
  return withRequestId;
3287
3320
  }
3288
- function responseWithRequestId(response, requestId, closeConnection) {
3321
+ function responseWithRequestId(response, requestId, closeConnection, corsOrigin) {
3289
3322
  const headers = new Headers(response.headers);
3290
3323
  headers.set("x-request-id", requestId);
3324
+ if (corsOrigin) {
3325
+ headers.set("access-control-allow-origin", corsOrigin);
3326
+ if (corsOrigin !== "*") {
3327
+ headers.append("vary", "Origin");
3328
+ }
3329
+ } else {
3330
+ headers.delete("access-control-allow-origin");
3331
+ }
3291
3332
  if (closeConnection) {
3292
3333
  headers.set("connection", "close");
3293
3334
  }