ledung 1.2.0 → 1.4.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/CHANGELOG.md CHANGED
@@ -10,6 +10,7 @@ that changes `src/`, `web/` (not `web/dist/`) or `scripts/` without one (a commi
10
10
  is nothing user-facing in it).
11
11
 
12
12
  ## 2026-11-02
13
+ - A request whose address doesn't parse is now refused with a 400, also on the live connection, without an error in the log (Fluffy-Fluffy-and-Bouncy/Project-Ledung#467)
13
14
  - Started at sign-in on macOS or Linux, or from Ledung.app, Ledung now gets the same PATH as in your terminal, so Update Ledung, Ledung's GitHub features and the tools your sessions use work after a login too; a shell profile that hangs delays the start by about ten seconds instead of stopping it. Switch Start Ledung when I sign in off and on in Settings to get it (#464)
14
15
  - Settings → About has an update channel, Public or Dev: on Dev an npm-installed Ledung gets dev builds before they go to everyone, and Update & restart now checks npm and updates an npm install with one click (#461)
15
16
 
package/dist/cli.js CHANGED
File without changes
package/dist/main.js CHANGED
@@ -15318,6 +15318,7 @@ var LINK_PAIRINGS_KEPT = 20;
15318
15318
  var LINK_PAIR_TIMES_KEY = "link-pair-times";
15319
15319
  var WAITING_POLL_MS = 3e4;
15320
15320
  var WAITING_PUSH_MS = 1e3;
15321
+ var exercised463 = false;
15321
15322
  var LINKED_HERE = "Computers are linked and unlinked";
15322
15323
  var MAX_BODY2 = 1024 * 1024;
15323
15324
  var NOT_PAIRED = "This device is not paired with Ledung. Pair it from Settings \u2192 Phones in Ledung on the computer.";
@@ -15540,12 +15541,16 @@ var LedungServer = class {
15540
15541
  this.handle(req, res).catch((e) => this.fail(res, e));
15541
15542
  });
15542
15543
  server.on("upgrade", (req, socket, head) => {
15543
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
15544
- if (url.pathname !== "/ws" || !this.guard.hostAllowed(req) || !this.guard.originAllowed(req) || !this.guard.mayUse(req)) {
15545
- socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
15544
+ const refuse = (status) => {
15545
+ socket.end(`HTTP/1.1 ${status}\r
15546
+ Connection: close\r
15547
+ \r
15548
+ `);
15546
15549
  setTimeout(() => socket.destroy(), 1e3).unref();
15547
- return;
15548
- }
15550
+ };
15551
+ const url = requestUrl(req);
15552
+ if (!url) return refuse("400 Bad Request");
15553
+ if (url.pathname !== "/ws" || !this.guard.hostAllowed(req) || !this.guard.originAllowed(req) || !this.guard.mayUse(req)) return refuse("403 Forbidden");
15549
15554
  const device = this.guard.deviceOf(req)?.id ?? null;
15550
15555
  const viaPublic = this.guard.isPublic(req);
15551
15556
  this.wss.handleUpgrade(req, socket, head, (ws) => this.onClient(ws, device, viaPublic));
@@ -15697,6 +15702,7 @@ var LedungServer = class {
15697
15702
  if (this.remote) this.setRemote(this.remote);
15698
15703
  }
15699
15704
  close() {
15705
+ this.closed = true;
15700
15706
  if (this.waitingPoll) clearInterval(this.waitingPoll);
15701
15707
  if (this.waitingPush) clearTimeout(this.waitingPush);
15702
15708
  for (const ws of this.clients) ws.close(1001, "Ledung is shutting down.");
@@ -15815,8 +15821,9 @@ var LedungServer = class {
15815
15821
  return computers;
15816
15822
  }
15817
15823
  async handle(req, res) {
15824
+ const url = requestUrl(req);
15825
+ if (!url) throw new HttpError(400, "Bad request path.");
15818
15826
  if (!this.guard.hostAllowed(req)) throw new HttpError(403, "Unknown host.");
15819
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
15820
15827
  const remote = this.guard.isPublic(req);
15821
15828
  if (remote) {
15822
15829
  this.publicResponses.add(res);
@@ -16051,6 +16058,11 @@ var LedungServer = class {
16051
16058
  waitingThere = /* @__PURE__ */ new Map();
16052
16059
  waitingPoll = null;
16053
16060
  waitingPush = null;
16061
+ /**
16062
+ * Set by close: an exchange still on its way then changes nothing and starts no new one, as the database may be
16063
+ * closed by the time its answer comes (#463: it threw "database is not open" after a test's end).
16064
+ */
16065
+ closed = false;
16054
16066
  /** The count last sent to the linked computers, so an unchanged one isn't sent again. */
16055
16067
  waitingSent = null;
16056
16068
  /** What waits for the user here, as this computer's page counts it (App: questions, prompts, cards). */
@@ -16059,7 +16071,7 @@ var LedungServer = class {
16059
16071
  }
16060
16072
  /** Something that may change the count happened: sent to the linked computers shortly, if it did change. */
16061
16073
  waitingChanged() {
16062
- if (this.waitingPush || !this.linked || !this.servers.length) return;
16074
+ if (this.closed || this.waitingPush || !this.linked || !this.servers.length) return;
16063
16075
  this.waitingPush = setTimeout(() => {
16064
16076
  this.waitingPush = null;
16065
16077
  if (this.waitingNow() !== this.waitingSent) this.exchangeWaiting();
@@ -16069,7 +16081,7 @@ var LedungServer = class {
16069
16081
  /** Sends this computer's count to every linked one and keeps theirs; one that doesn't answer loses its badge. */
16070
16082
  exchangeWaiting() {
16071
16083
  const linked = this.linked;
16072
- if (!linked) return;
16084
+ if (!linked || this.closed) return;
16073
16085
  const count = this.waitingNow();
16074
16086
  this.waitingSent = count;
16075
16087
  for (const link of linked.list()) {
@@ -16078,6 +16090,13 @@ var LedungServer = class {
16078
16090
  }
16079
16091
  /** A linked computer's count, or undefined when it is offline: the open pages get its tab's badge at once. */
16080
16092
  noteWaiting(id, raw) {
16093
+ if (this.closed) {
16094
+ if (!exercised463) {
16095
+ exercised463 = true;
16096
+ exercisedLine("EXERCISED #463 a linked computer's answer after close was let go");
16097
+ }
16098
+ return;
16099
+ }
16081
16100
  const count = typeof raw === "number" && Number.isInteger(raw) && raw >= 0 && raw < 1e6 ? raw : void 0;
16082
16101
  if (this.waitingThere.get(id) === count) return;
16083
16102
  if (count === void 0) this.waitingThere.delete(id);
@@ -16963,6 +16982,15 @@ var LedungServer = class {
16963
16982
  }
16964
16983
  };
16965
16984
  var bodies = /* @__PURE__ */ new WeakMap();
16985
+ var badPathTraced = false;
16986
+ function requestUrl(req) {
16987
+ const url = URL.parse(req.url ?? "/", "http://127.0.0.1");
16988
+ if (!url && !badPathTraced) {
16989
+ badPathTraced = true;
16990
+ exercisedLine("EXERCISED #467 refused a request path that does not parse");
16991
+ }
16992
+ return url;
16993
+ }
16966
16994
  function fromThisComputer(req) {
16967
16995
  const from = req.socket.remoteAddress ?? "";
16968
16996
  return from === "::1" || from.startsWith("127.") || from.startsWith("::ffff:127.");
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "ledung",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "ledung",
9
- "version": "1.2.0",
9
+ "version": "1.4.0",
10
10
  "license": "AGPL-3.0-only",
11
11
  "dependencies": {
12
12
  "@anthropic-ai/claude-agent-sdk": "0.3.295",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ledung",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "license": "AGPL-3.0-only",
5
5
  "author": "Project Ledung <martin@projectledung.com>",
6
6
  "description": "Runs several Claude Code sessions locally and shows them in one place.",