@moritzwilksch/diffle 0.1.3 → 0.1.4

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 CHANGED
@@ -62,11 +62,18 @@ Opening `/diffle` redirects to `/diffle/`. Configure the proxy to strip that pre
62
62
  when forwarding to diffle, including WebSocket upgrades at `/diffle/ws`. No build
63
63
  or CLI base-path setting is needed.
64
64
 
65
- Run diffle with `--no-open` for a proxy-managed session. Its Host/Origin checks also
66
- apply behind a proxy: when forwarding to the default loopback listener, use the
67
- upstream Host (for example `127.0.0.1:4966`). The proxy must validate any browser
68
- Origin against its public origin before rewriting it to the upstream origin for
69
- both HTTP requests and WebSocket upgrades.
65
+ Run diffle with `--no-open` for a proxy-managed session. If the proxy preserves its
66
+ public Host or Origin headers, explicitly trust its public origin (without the path):
67
+
68
+ ```bash
69
+ diffle working -H 0.0.0.0 --no-open --allowed-origin https://proxy.example
70
+ ```
71
+
72
+ This applies to both HTTP requests and WebSocket upgrades. The proxy can preserve
73
+ the public Host or rewrite it to an allowed upstream Host, such as `127.0.0.1:4966`.
74
+ Other browser origins remain rejected; forwarded headers do not grant trust.
75
+ Without this setting, the proxy must use an allowed upstream Host and validate any
76
+ browser Origin against its public origin before rewriting it to the upstream origin.
70
77
 
71
78
  ## Shell completions
72
79
 
@@ -3951,7 +3951,7 @@ function generateCompletion(program, { shell, executable = program.name() }) {
3951
3951
  }
3952
3952
  //#endregion
3953
3953
  //#region package.json
3954
- var version$1 = "0.1.3";
3954
+ var version$1 = "0.1.4";
3955
3955
  //#endregion
3956
3956
  //#region src/server/comments/anchor.ts
3957
3957
  function splitLines(contents) {
@@ -15781,35 +15781,31 @@ const LOOPBACK = /* @__PURE__ */ new Set([
15781
15781
  "localhost"
15782
15782
  ]);
15783
15783
  /**
15784
- * Host/Origin policy for API and WebSocket requests. Pure given its inputs;
15785
- * `bindHost` is the `--host` value. An Origin, when sent, must name the same
15786
- * host:port as Host (blocks cross-site pages). On a loopback bind, Host must be
15787
- * loopback too. On any other bind, Host must be an IP literal, loopback, the
15788
- * bind host, or one of `names` (the machine's own hostnames by default). Both
15789
- * rules block DNS rebinding: an attacker's domain is never an IP literal and
15790
- * never one of ours.
15784
+ * Restricts API and WebSocket requests to local hosts and the configured public origin.
15785
+ * Proxies may preserve the public Host or rewrite it to an allowed upstream Host.
15786
+ * Forwarded headers never grant trust; a public Origin must be explicitly configured.
15791
15787
  */
15792
- function requestGuard(bindHost, names = ownNames()) {
15788
+ function requestGuard(bindHost, names = ownNames(), allowedOrigin) {
15793
15789
  const loopback = LOOPBACK.has(bindHost);
15794
15790
  const allowed = /* @__PURE__ */ new Set([
15795
15791
  ...LOOPBACK,
15796
15792
  bindHost.toLowerCase(),
15797
15793
  ...names.map((n) => n.toLowerCase())
15798
15794
  ]);
15795
+ const publicHost = allowedOrigin == null ? void 0 : new URL(allowedOrigin).host;
15799
15796
  return ({ host, origin }) => {
15800
15797
  if (!host) return false;
15801
- if (origin != null) {
15802
- let originHost;
15803
- try {
15804
- originHost = new URL(origin).host;
15805
- } catch {
15806
- return false;
15807
- }
15808
- if (originHost !== host) return false;
15809
- }
15810
15798
  const name = hostnameOf(host).toLowerCase();
15811
- if (loopback) return LOOPBACK.has(name);
15812
- return isIP(name) !== 0 || allowed.has(name);
15799
+ const localHost = loopback ? LOOPBACK.has(name) : isIP(name) !== 0 || allowed.has(name);
15800
+ const proxyHost = publicHost != null && host.toLowerCase() === publicHost;
15801
+ if (!localHost && !proxyHost) return false;
15802
+ if (origin == null || allowedOrigin != null && origin === allowedOrigin) return true;
15803
+ if (proxyHost) return false;
15804
+ try {
15805
+ return new URL(origin).host === host;
15806
+ } catch {
15807
+ return false;
15808
+ }
15813
15809
  };
15814
15810
  }
15815
15811
  /** The machine's hostname, its short form, and its mDNS name, so LAN visitors can use any of them. */
@@ -16501,7 +16497,7 @@ var Server = class {
16501
16497
  }
16502
16498
  async listen() {
16503
16499
  const app = new Hono();
16504
- const guard = requestGuard(this.opts.host);
16500
+ const guard = requestGuard(this.opts.host, void 0, this.opts.allowedOrigin);
16505
16501
  app.use("/api/*", async (c, next) => {
16506
16502
  if (!guard({
16507
16503
  host: c.req.header("host"),
@@ -21067,6 +21063,16 @@ function intArg(min, max) {
21067
21063
  }
21068
21064
  const parsePort = intArg(0, 65535);
21069
21065
  const parseContext = intArg(0, 1e4);
21066
+ /** Accepts an HTTP(S) origin without credentials, a path prefix, query, or fragment. */
21067
+ function parseAllowedOrigin(raw) {
21068
+ try {
21069
+ const url = new URL(raw);
21070
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash || !/^https?:\/\/[^/?#]+\/?$/i.test(raw)) throw new Error("invalid origin");
21071
+ return url.origin;
21072
+ } catch {
21073
+ throw new InvalidArgumentError("expected an HTTP(S) origin, e.g. https://proxy.example (without a path)");
21074
+ }
21075
+ }
21070
21076
  /** Commander parser for a language diffle knows; anything else lists the ones it does. */
21071
21077
  function parseLanguage(raw) {
21072
21078
  const language = raw.trim().toLowerCase();
@@ -21238,7 +21244,7 @@ const c = {
21238
21244
  bold: paint("1"),
21239
21245
  dim: paint("2")
21240
21246
  };
21241
- const program = new Command().name("diffle").description("Review a git diff in the browser and export line comments as an agent prompt.").version(version$1, "-v, --version", "print the version and exit").addOption(completionHint(new Option("-C <path>", "run as if started in <path> (any directory inside a git worktree)"), { kind: "directory" })).option("-p, --port <port>", `port to listen on (default: ${DEFAULT_PORT}, or the next free one; 0 = random)`, parsePort).option("-H, --host <host>", "address to bind; use 0.0.0.0 to expose on the network", "127.0.0.1").option("--no-open", "do not open a browser").option("--keep-alive", "keep the server running after all browser tabs close").option("--no-watch", "do not watch for changes").addOption(new Option("--auto-viewed <glob>", "mark matching files viewed for this session (repeatable)").argParser(collect).default([], "none")).option("-U, --context <n>", "context lines around changes for this session (default: config, 5)", parseContext).addOption(new Option("--lsp <language=command>", "language server command for one language, started for this run (repeatable); by default the diff's languages are served by whatever is on PATH").argParser(collectLspOverride).default(true, "the diff's languages, resolved on PATH")).option("--no-lsp", "do not start any language server").option("--timing", "print startup phase timings to stderr").option("--dev", "serve the client through Vite (development)", process.env.DIFFLE_DEV === "1" ? true : void 0).addArgument(completionHint(new Argument("[revs...]", "git-diff style revisions: <rev> | <a>..<b> | <a>...<b> | <a> <b>"), {
21247
+ const program = new Command().exitOverride().name("diffle").description("Review a git diff in the browser and export line comments as an agent prompt.").version(version$1, "-v, --version", "print the version and exit").addOption(completionHint(new Option("-C <path>", "run as if started in <path> (any directory inside a git worktree)"), { kind: "directory" })).option("-p, --port <port>", `port to listen on (default: ${DEFAULT_PORT}, or the next free one; 0 = random)`, parsePort).option("-H, --host <host>", "address to bind; use 0.0.0.0 to expose on the network", "127.0.0.1").option("--allowed-origin <origin>", "trusted public HTTP(S) origin behind a reverse proxy (without a path)", parseAllowedOrigin).option("--no-open", "do not open a browser").option("--keep-alive", "keep the server running after all browser tabs close").option("--no-watch", "do not watch for changes").addOption(new Option("--auto-viewed <glob>", "mark matching files viewed for this session (repeatable)").argParser(collect).default([], "none")).option("-U, --context <n>", "context lines around changes for this session (default: config, 5)", parseContext).addOption(new Option("--lsp <language=command>", "language server command for one language, started for this run (repeatable); by default the diff's languages are served by whatever is on PATH").argParser(collectLspOverride).default(true, "the diff's languages, resolved on PATH")).option("--no-lsp", "do not start any language server").option("--timing", "print startup phase timings to stderr").option("--dev", "serve the client through Vite (development)", process.env.DIFFLE_DEV === "1" ? true : void 0).addArgument(completionHint(new Argument("[revs...]", "git-diff style revisions: <rev> | <a>..<b> | <a>...<b> | <a> <b>"), {
21242
21248
  kind: "choices",
21243
21249
  values: ["working", "pr"]
21244
21250
  })).addHelpText("after", `
@@ -21374,6 +21380,7 @@ async function serve(req, opts, repo, closeRepo, config, timing) {
21374
21380
  port: opts.port ?? 4966,
21375
21381
  probe: opts.port == null,
21376
21382
  host: opts.host,
21383
+ allowedOrigin: opts.allowedOrigin,
21377
21384
  dev: opts.dev || !hasClientBuild()
21378
21385
  });
21379
21386
  let stopBrowserWatch = () => {};
@@ -21471,9 +21478,8 @@ function startLsp(repo, session, hub, overrides, preload) {
21471
21478
  });
21472
21479
  return lsp;
21473
21480
  }
21474
- program.exitOverride();
21475
21481
  program.parseAsync(process.argv).catch((e) => {
21476
- if (e instanceof CommanderError) process.exit(e.exitCode);
21482
+ if (e instanceof CommanderError) process.exit(e.exitCode === 0 ? 0 : 2);
21477
21483
  console.error(`${c.red("✖")} ${e instanceof Error ? e.message : e}`);
21478
21484
  process.exit(e instanceof RevspecError || e instanceof GitError || e instanceof GithubError ? 2 : 1);
21479
21485
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moritzwilksch/diffle",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Local git diff reviewer in the browser with line comments exported as agent prompts",
5
5
  "keywords": [
6
6
  "agent",