@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/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  main,
7
7
  trimTrailingSlash,
8
8
  truncatedResponseText
9
- } from "./chunk-7GSQVYYT.js";
9
+ } from "./chunk-JU6F5L34.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import { spawn } from "child_process";
@@ -2107,7 +2107,8 @@ async function getVersion() {
2107
2107
  // src/server.ts
2108
2108
  var DEFAULT_HOST = "127.0.0.1";
2109
2109
  var DEFAULT_PORT = 4141;
2110
- var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
2110
+ var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Cross-origin browser requests are blocked unless the Origin is loopback or listed in HOOPILOT_ALLOWED_ORIGINS.";
2111
+ var WELL_KNOWN_DEMO_API_KEYS = /* @__PURE__ */ new Set(["local-key"]);
2111
2112
  var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
2112
2113
  var JSON_OBJECT_MESSAGE = "Request body must be a JSON object.";
2113
2114
  var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
@@ -2123,6 +2124,7 @@ var RequestBodyTooLargeError = class extends Error {
2123
2124
  function createHoopilotHandler(options = {}) {
2124
2125
  const client = new CopilotClient(options);
2125
2126
  const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
2127
+ const allowedOrigins = parseAllowedOrigins(options.env);
2126
2128
  const logger = serverLogger(options);
2127
2129
  const metrics = options.metrics ?? new MetricsRegistry();
2128
2130
  const readUsage = createUsageReader(client, metrics);
@@ -2142,7 +2144,10 @@ function createHoopilotHandler(options = {}) {
2142
2144
  route
2143
2145
  });
2144
2146
  metrics.startRequest();
2147
+ const origin = request.headers.get("origin")?.trim() || void 0;
2148
+ const corsOrigin = resolveCorsAllowOrigin(origin, allowedOrigins);
2145
2149
  const finish = (response) => finishResponse(response, {
2150
+ corsOrigin,
2146
2151
  logger: requestLogger,
2147
2152
  method: request.method,
2148
2153
  metrics,
@@ -2152,11 +2157,11 @@ function createHoopilotHandler(options = {}) {
2152
2157
  closeConnection: bufferProxyBodies,
2153
2158
  trackStreamingBody: !bufferProxyBodies
2154
2159
  });
2155
- const browserOrigin = forbiddenBrowserOrigin(request, apiKey);
2160
+ const browserOrigin = forbiddenBrowserOrigin(origin, request, allowedOrigins);
2156
2161
  if (browserOrigin) {
2157
2162
  requestLogger.warn(
2158
2163
  { event: "http.request.forbidden_origin", origin: browserOrigin },
2159
- "blocked unauthenticated browser-origin request"
2164
+ "blocked cross-origin browser request"
2160
2165
  );
2161
2166
  return finish(jsonError(403, "forbidden_origin", FORBIDDEN_BROWSER_ORIGIN_MESSAGE));
2162
2167
  }
@@ -2282,10 +2287,17 @@ function startHoopilotServer(options = {}) {
2282
2287
  const port = normalizeServerPort(options.port ?? envValue(options.env?.PORT) ?? DEFAULT_PORT);
2283
2288
  const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
2284
2289
  const allowUnauthenticated = options.allowUnauthenticated ?? envValue(options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED) === "1";
2285
- if (!isLoopbackHost(host) && !apiKey && !allowUnauthenticated) {
2286
- throw new Error(
2287
- "Refusing to listen on a non-loopback host without HOOPILOT_API_KEY. Set an API key or pass --allow-unauthenticated."
2288
- );
2290
+ if (!isLoopbackHost(host)) {
2291
+ if (!apiKey && !allowUnauthenticated) {
2292
+ throw new Error(
2293
+ "Refusing to listen on a non-loopback host without HOOPILOT_API_KEY. Set an API key or pass --allow-unauthenticated."
2294
+ );
2295
+ }
2296
+ if (apiKey && isWellKnownDemoApiKey(apiKey)) {
2297
+ throw new Error(
2298
+ "Refusing to listen on a non-loopback host with a well-known demo HOOPILOT_API_KEY. Set a strong, unique API key."
2299
+ );
2300
+ }
2289
2301
  }
2290
2302
  const server = Bun.serve({
2291
2303
  fetch: createHoopilotHandler({
@@ -2598,7 +2610,6 @@ function corsHeaders() {
2598
2610
  return {
2599
2611
  "access-control-allow-headers": "anthropic-beta, anthropic-dangerous-direct-browser-access, anthropic-version, authorization, content-type, x-api-key, x-request-id",
2600
2612
  "access-control-allow-methods": "GET, POST, OPTIONS",
2601
- "access-control-allow-origin": "*",
2602
2613
  "access-control-expose-headers": "x-request-id"
2603
2614
  };
2604
2615
  }
@@ -2610,17 +2621,34 @@ function isAuthorized(request, apiKey) {
2610
2621
  const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
2611
2622
  return bearer === apiKey || request.headers.get("x-api-key") === apiKey;
2612
2623
  }
2613
- function forbiddenBrowserOrigin(request, apiKey) {
2614
- if (apiKey) {
2615
- return void 0;
2616
- }
2617
- const origin = request.headers.get("origin")?.trim();
2624
+ function forbiddenBrowserOrigin(origin, request, allowedOrigins) {
2618
2625
  if (origin) {
2619
- return isLoopbackOrigin(origin) ? void 0 : origin;
2626
+ return isAllowedOrigin(origin, allowedOrigins) ? void 0 : origin;
2620
2627
  }
2621
2628
  const fetchSite = request.headers.get("sec-fetch-site")?.toLowerCase();
2622
2629
  return fetchSite === "cross-site" ? "cross-site" : void 0;
2623
2630
  }
2631
+ function parseAllowedOrigins(env) {
2632
+ const raw = envValue(env?.HOOPILOT_ALLOWED_ORIGINS);
2633
+ if (!raw) {
2634
+ return /* @__PURE__ */ new Set();
2635
+ }
2636
+ return new Set(
2637
+ raw.split(",").map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0)
2638
+ );
2639
+ }
2640
+ function isAllowedOrigin(origin, allowedOrigins) {
2641
+ return isLoopbackOrigin(origin) || allowedOrigins.has(origin.toLowerCase());
2642
+ }
2643
+ function resolveCorsAllowOrigin(origin, allowedOrigins) {
2644
+ if (!origin) {
2645
+ return "*";
2646
+ }
2647
+ return isAllowedOrigin(origin, allowedOrigins) ? origin : void 0;
2648
+ }
2649
+ function isWellKnownDemoApiKey(apiKey) {
2650
+ return WELL_KNOWN_DEMO_API_KEYS.has(apiKey.trim().toLowerCase());
2651
+ }
2624
2652
  function isUpstreamAuthStatus(status) {
2625
2653
  return status === 401 || status === 403;
2626
2654
  }
@@ -2680,7 +2708,12 @@ function shouldBufferProxyBodies(mode) {
2680
2708
  return process.platform === "win32" && IS_STANDALONE_BINARY;
2681
2709
  }
2682
2710
  function finishResponse(response, options) {
2683
- const withRequestId = responseWithRequestId(response, options.requestId, options.closeConnection);
2711
+ const withRequestId = responseWithRequestId(
2712
+ response,
2713
+ options.requestId,
2714
+ options.closeConnection,
2715
+ options.corsOrigin
2716
+ );
2684
2717
  const stream = isStreamingResponse(withRequestId);
2685
2718
  const status = withRequestId.status;
2686
2719
  const complete = () => {
@@ -2698,9 +2731,17 @@ function finishResponse(response, options) {
2698
2731
  complete();
2699
2732
  return withRequestId;
2700
2733
  }
2701
- function responseWithRequestId(response, requestId, closeConnection) {
2734
+ function responseWithRequestId(response, requestId, closeConnection, corsOrigin) {
2702
2735
  const headers = new Headers(response.headers);
2703
2736
  headers.set("x-request-id", requestId);
2737
+ if (corsOrigin) {
2738
+ headers.set("access-control-allow-origin", corsOrigin);
2739
+ if (corsOrigin !== "*") {
2740
+ headers.append("vary", "Origin");
2741
+ }
2742
+ } else {
2743
+ headers.delete("access-control-allow-origin");
2744
+ }
2704
2745
  if (closeConnection) {
2705
2746
  headers.set("connection", "close");
2706
2747
  }
@@ -3473,7 +3514,7 @@ async function main2(argv = Bun.argv.slice(2)) {
3473
3514
  if (await printMetaOption(args2)) {
3474
3515
  return;
3475
3516
  }
3476
- args2.logger = commandLogger(args2, "login");
3517
+ args2.logger = commandLogger(args2, "login", args2.printToken ? process.stderr : void 0);
3477
3518
  await runLogin(args2);
3478
3519
  return;
3479
3520
  }
@@ -3556,6 +3597,10 @@ function parseArgs(argv) {
3556
3597
  args.noUpdateCheck = true;
3557
3598
  continue;
3558
3599
  }
3600
+ if (arg === "--print-key" || arg === "--print-token") {
3601
+ args.printToken = true;
3602
+ continue;
3603
+ }
3559
3604
  if (!arg.startsWith("-")) {
3560
3605
  throw new Error(`Unknown argument: ${arg}.`);
3561
3606
  }
@@ -3629,14 +3674,16 @@ function readApiKeyFile(path) {
3629
3674
  }
3630
3675
  async function runLogin(options = {}) {
3631
3676
  const logger = options.logger?.child({ component: "auth" }) ?? noopLogger;
3677
+ const status = loginStatusLogger(Boolean(options.printToken));
3632
3678
  logger.debug({ event: "auth.login.started" }, "starting github copilot browser login");
3633
- console.log("Starting GitHub Copilot browser login...");
3634
- const login = await githubCopilotDeviceLogin({
3679
+ status.info("Starting GitHub Copilot browser login...");
3680
+ const deviceLogin = options.deviceLogin ?? githubCopilotDeviceLogin;
3681
+ const login = await deviceLogin({
3635
3682
  env: options.env,
3636
- logger: console,
3683
+ logger: status,
3637
3684
  openBrowser: openBrowserBestEffort
3638
3685
  });
3639
- console.log("Checking GitHub Copilot access...");
3686
+ status.info("Checking GitHub Copilot access...");
3640
3687
  const access = await verifyCopilotOAuthToken(login.token, options);
3641
3688
  logger.debug(
3642
3689
  { apiBaseUrl: access.apiBaseUrl, event: "auth.login.verified" },
@@ -3653,8 +3700,11 @@ async function runLogin(options = {}) {
3653
3700
  path
3654
3701
  );
3655
3702
  logger.debug({ authStorePath: path, event: "auth.login.stored" }, "copilot credential stored");
3656
- console.log(`Copilot OAuth credential stored at ${path}`);
3657
- console.log("Copilot authentication ready.");
3703
+ status.info(`Copilot OAuth credential stored at ${path}`);
3704
+ status.info("Copilot authentication ready.");
3705
+ if (options.printToken) {
3706
+ console.log(login.token);
3707
+ }
3658
3708
  }
3659
3709
  async function runModels(options = {}) {
3660
3710
  const logger = options.logger?.child({ component: "models" }) ?? noopLogger;
@@ -3823,13 +3873,28 @@ function modelIdsFromResponse(body) {
3823
3873
  function withRuntimeEnv(args) {
3824
3874
  return { ...args, env: process.env };
3825
3875
  }
3826
- function commandLogger(args, command) {
3876
+ function commandLogger(args, command, stream) {
3827
3877
  return createHoopilotLogger({
3828
3878
  env: args.env,
3829
3879
  format: args.logFormat,
3830
- level: args.logLevel
3880
+ level: args.logLevel,
3881
+ stream
3831
3882
  }).child({ command, component: "cli" });
3832
3883
  }
3884
+ function loginStatusLogger(writeSecretsToStdout) {
3885
+ if (writeSecretsToStdout) {
3886
+ return {
3887
+ error: (message) => console.error(message),
3888
+ info: (message) => console.error(message),
3889
+ warn: (message) => console.error(message)
3890
+ };
3891
+ }
3892
+ return {
3893
+ error: (message) => console.error(message),
3894
+ info: (message) => console.log(message),
3895
+ warn: (message) => console.warn(message)
3896
+ };
3897
+ }
3833
3898
  function helpText(version) {
3834
3899
  return `hoopilot ${version}
3835
3900
 
@@ -3863,6 +3928,7 @@ Options:
3863
3928
  --api-key-file <path> Read the local API key from a file instead of argv
3864
3929
  --auth-file <path> OAuth credential store path
3865
3930
  --copilot-api-base-url <url> Copilot API base URL override
3931
+ --print-key Login: print the received OAuth token to stdout
3866
3932
  --log-level <level> trace, debug, info, warn, error, fatal, or silent
3867
3933
  --log-format <format> json or pretty. Default: pretty
3868
3934
  --stream-mode <mode> auto, live, or buffer. Auto buffers Windows standalone streams.
@@ -3895,6 +3961,7 @@ export {
3895
3961
  main2 as main,
3896
3962
  openBrowserBestEffort,
3897
3963
  parseArgs,
3964
+ runLogin,
3898
3965
  runModels,
3899
3966
  runUsage,
3900
3967
  verifyCopilotOAuthToken