@weaveprotocol/cli 0.1.1 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/weave.mjs +202 -69
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,7 +30,7 @@ To have `weave` everywhere:
30
30
  npm install -g @weaveprotocol/cli # or run it without installing: npx @weaveprotocol/cli <command>
31
31
  ```
32
32
 
33
- From this repo: `cd cli && npm install && npm run bundle && npm link`, or a
33
+ From this repo: `npm install` at the repo root (the CLI is a workspace), then `cd cli && npm run bundle && npm link`, or a
34
34
  binary with no Node at all: `bun build.ts --native`.
35
35
 
36
36
  ```bash
package/dist/weave.mjs CHANGED
@@ -10687,6 +10687,201 @@ async function unlock(home, account, secret) {
10687
10687
  // src/serve.ts
10688
10688
  import { createServer } from "node:http";
10689
10689
  import { WebSocketServer } from "ws";
10690
+
10691
+ // ../server/relay.mjs
10692
+ import { createHmac } from "node:crypto";
10693
+ var MAX_MESSAGE_BYTES = 64 * 1024;
10694
+ var MAX_CONNECTIONS = 5e3;
10695
+ var MAX_CONNECTIONS_PER_IP = 32;
10696
+ var MAX_ROOMS = 1e4;
10697
+ var MAX_ROOMS_PER_SOCKET = 256;
10698
+ var MAX_PEERS_PER_ROOM = 64;
10699
+ var MAX_ROOM_LENGTH = 128;
10700
+ var MAX_DID_LENGTH = 256;
10701
+ var RATE_PER_SECOND = 50;
10702
+ var RATE_BURST = 100;
10703
+ var HEARTBEAT_MS = 15e3;
10704
+ var MAX_BUFFERED_BYTES = 1024 * 1024;
10705
+ var ROUTED = /* @__PURE__ */ new Set(["offer", "answer", "candidate"]);
10706
+ var DID_PATTERN = /^did:key:z[1-9A-HJ-NP-Za-km-z]+$/;
10707
+ var CLOSE_DID_TAKEN = 4009;
10708
+ function turnFromEnv(env) {
10709
+ const secret = env.TURN_SECRET ?? "";
10710
+ const urls = (env.TURN_URLS ?? "").split(",").map((url) => url.trim()).filter(Boolean);
10711
+ if (!secret || urls.length === 0) return void 0;
10712
+ return { secret, urls, ttlSeconds: Number(env.TURN_TTL_SECONDS ?? 4 * 3600) };
10713
+ }
10714
+ function createRelay(options = {}) {
10715
+ const { turn } = options;
10716
+ const log = options.log ?? (() => {
10717
+ });
10718
+ const rooms = /* @__PURE__ */ new Map();
10719
+ const clients = /* @__PURE__ */ new Set();
10720
+ const perIp = /* @__PURE__ */ new Map();
10721
+ function canEnter(room) {
10722
+ if (typeof room !== "string" || room.length === 0 || room.length > MAX_ROOM_LENGTH) return false;
10723
+ const peers = rooms.get(room);
10724
+ return peers ? peers.size < MAX_PEERS_PER_ROOM : rooms.size < MAX_ROOMS;
10725
+ }
10726
+ function accept(ws, legacyRoom, ip) {
10727
+ const client = { ws, legacyRoom, ip, did: null, rooms: /* @__PURE__ */ new Set(), alive: true, tokens: RATE_BURST, refilled: Date.now() };
10728
+ clients.add(client);
10729
+ perIp.set(ip, (perIp.get(ip) ?? 0) + 1);
10730
+ ws.on("pong", () => client.alive = true);
10731
+ ws.on("message", (data, isBinary) => {
10732
+ if (!withinBudget(client)) {
10733
+ ws.terminate();
10734
+ return;
10735
+ }
10736
+ if (!isBinary) handleMessage(client, data.toString("utf8"));
10737
+ });
10738
+ let dropped = false;
10739
+ const drop = () => {
10740
+ if (dropped) return;
10741
+ dropped = true;
10742
+ clients.delete(client);
10743
+ const left = (perIp.get(ip) ?? 1) - 1;
10744
+ if (left > 0) perIp.set(ip, left);
10745
+ else perIp.delete(ip);
10746
+ for (const room of [...client.rooms]) leave(client, room);
10747
+ };
10748
+ ws.on("close", drop);
10749
+ ws.on("error", () => ws.terminate());
10750
+ }
10751
+ function handleMessage(client, raw) {
10752
+ let message;
10753
+ try {
10754
+ message = JSON.parse(raw);
10755
+ } catch {
10756
+ return;
10757
+ }
10758
+ if (!message || typeof message.type !== "string") return;
10759
+ const room = message.room ?? client.legacyRoom;
10760
+ if (message.type === "join") {
10761
+ if (client.did ? message.from !== client.did : !isDid(message.from)) return;
10762
+ if (client.rooms.has(room) || client.rooms.size >= MAX_ROOMS_PER_SOCKET || !canEnter(room)) return;
10763
+ if (findPeer(room, message.from)) {
10764
+ client.ws.close(CLOSE_DID_TAKEN, "DID already in room");
10765
+ return;
10766
+ }
10767
+ const first = !client.did;
10768
+ client.did = message.from;
10769
+ client.rooms.add(room);
10770
+ if (!rooms.has(room)) rooms.set(room, /* @__PURE__ */ new Set());
10771
+ rooms.get(room).add(client);
10772
+ announce(client, room, "join");
10773
+ if (first) offerTurn(client);
10774
+ log(`peer joined room ${short(room)} (${rooms.get(room).size} present)`);
10775
+ return;
10776
+ }
10777
+ if (message.type === "ice") {
10778
+ if (client.did) offerTurn(client);
10779
+ return;
10780
+ }
10781
+ if (message.type === "leave") {
10782
+ if (client.rooms.has(room)) leave(client, room);
10783
+ return;
10784
+ }
10785
+ if (!client.did || !ROUTED.has(message.type) || typeof message.to !== "string") return;
10786
+ for (const shared of client.rooms) {
10787
+ const target = findPeer(shared, message.to);
10788
+ if (target && target !== client) {
10789
+ send(target, JSON.stringify({ type: message.type, from: client.did, to: message.to, payload: message.payload }));
10790
+ return;
10791
+ }
10792
+ }
10793
+ }
10794
+ function offerTurn(client) {
10795
+ if (!turn) return;
10796
+ const expires = Math.floor(Date.now() / 1e3) + turn.ttlSeconds;
10797
+ const username = String(expires);
10798
+ const credential = createHmac("sha1", turn.secret).update(username).digest("base64");
10799
+ send(client, JSON.stringify({ type: "ice", payload: { servers: [{ urls: turn.urls, username, credential }], expiresAt: expires * 1e3 } }));
10800
+ }
10801
+ function leave(client, room) {
10802
+ client.rooms.delete(room);
10803
+ const peers = rooms.get(room);
10804
+ peers?.delete(client);
10805
+ if (peers?.size === 0) rooms.delete(room);
10806
+ announce(client, room, "leave");
10807
+ log(`peer left room ${short(room)}`);
10808
+ }
10809
+ function announce(sender, room, type) {
10810
+ const payload = JSON.stringify({ type, from: sender.did, room });
10811
+ for (const peer of rooms.get(room) ?? []) {
10812
+ if (peer !== sender) send(peer, payload);
10813
+ }
10814
+ }
10815
+ function send(peer, text) {
10816
+ if (peer.ws.readyState !== peer.ws.OPEN) return;
10817
+ if (peer.ws.bufferedAmount > MAX_BUFFERED_BYTES) {
10818
+ peer.ws.terminate();
10819
+ return;
10820
+ }
10821
+ peer.ws.send(text);
10822
+ }
10823
+ function findPeer(room, did) {
10824
+ for (const peer of rooms.get(room) ?? []) {
10825
+ if (peer.did === did) return peer;
10826
+ }
10827
+ return null;
10828
+ }
10829
+ function withinBudget(client) {
10830
+ const now = Date.now();
10831
+ client.tokens = Math.min(RATE_BURST, client.tokens + (now - client.refilled) / 1e3 * RATE_PER_SECOND);
10832
+ client.refilled = now;
10833
+ if (client.tokens < 1) return false;
10834
+ client.tokens -= 1;
10835
+ return true;
10836
+ }
10837
+ const heartbeat = setInterval(() => {
10838
+ for (const client of clients) {
10839
+ if (!client.alive) {
10840
+ client.ws.terminate();
10841
+ continue;
10842
+ }
10843
+ client.alive = false;
10844
+ client.ws.ping();
10845
+ }
10846
+ }, HEARTBEAT_MS);
10847
+ heartbeat.unref();
10848
+ return {
10849
+ /**
10850
+ * Takes an HTTP upgrade request. Everything that can be refused is refused
10851
+ * here, before a socket is opened.
10852
+ *
10853
+ * @param {import('ws').WebSocketServer} wss A `ws` server made with `noServer: true` and `maxPayload: MAX_MESSAGE_BYTES`
10854
+ */
10855
+ upgrade(wss, req, socket, head) {
10856
+ socket.on("error", () => socket.destroy());
10857
+ const legacyRoom = new URL(req.url ?? "/", "http://relay").searchParams.get("room");
10858
+ const ip = clientIp(req);
10859
+ const refusal2 = legacyRoom !== null && !canEnter(legacyRoom) ? [503, "Room full"] : clients.size >= MAX_CONNECTIONS ? [503, "Relay full"] : (perIp.get(ip) ?? 0) >= MAX_CONNECTIONS_PER_IP ? [429, "Too many connections"] : null;
10860
+ if (refusal2) {
10861
+ socket.end(`HTTP/1.1 ${refusal2[0]} ${refusal2[1]}\r
10862
+ Connection: close\r
10863
+ Content-Length: 0\r
10864
+ \r
10865
+ `);
10866
+ return;
10867
+ }
10868
+ wss.handleUpgrade(req, socket, head, (ws) => accept(ws, legacyRoom, ip));
10869
+ },
10870
+ /** Drops every socket and stops the heartbeat */
10871
+ close() {
10872
+ clearInterval(heartbeat);
10873
+ for (const client of clients) client.ws.terminate();
10874
+ }
10875
+ };
10876
+ }
10877
+ var isDid = (value) => typeof value === "string" && value.length <= MAX_DID_LENGTH && DID_PATTERN.test(value);
10878
+ function clientIp(req) {
10879
+ const forwarded = process.env.FLY_APP_NAME ? req.headers["fly-client-ip"] : void 0;
10880
+ return typeof forwarded === "string" && forwarded || req.socket.remoteAddress || "unknown";
10881
+ }
10882
+ var short = (room) => room.length > 12 ? `${room.slice(0, 12)}\u2026` : room;
10883
+
10884
+ // src/serve.ts
10690
10885
  function createSpacePeers() {
10691
10886
  const sockets = /* @__PURE__ */ new Map();
10692
10887
  const listeners = {};
@@ -10751,84 +10946,21 @@ function createInboundPeers() {
10751
10946
  count: () => spaces.size
10752
10947
  };
10753
10948
  }
10754
- var RELAY_MAX_ROOM_LENGTH = 128;
10755
- var RELAY_MAX_PEERS_PER_ROOM = 64;
10756
- var RELAY_MESSAGES_PER_SECOND = 50;
10757
- var RELAY_BURST = 100;
10758
- var DID_PATTERN = /^did:key:z[1-9A-HJ-NP-Za-km-z]{1,250}$/;
10759
- function createRelay() {
10760
- const rooms = /* @__PURE__ */ new Map();
10761
- const broadcast = (sender, payload) => {
10762
- for (const peer of rooms.get(sender.room) ?? []) {
10763
- if (peer !== sender && peer.did && peer.socket.readyState === peer.socket.OPEN) peer.socket.send(payload);
10764
- }
10765
- };
10766
- return {
10767
- rooms,
10768
- accept(socket, room) {
10769
- if (room.length > RELAY_MAX_ROOM_LENGTH || (rooms.get(room)?.size ?? 0) >= RELAY_MAX_PEERS_PER_ROOM) {
10770
- socket.close(1008, "room refused");
10771
- return;
10772
- }
10773
- const client = { socket, room, did: null };
10774
- if (!rooms.has(room)) rooms.set(room, /* @__PURE__ */ new Set());
10775
- rooms.get(room).add(client);
10776
- let tokens = RELAY_BURST;
10777
- let refilled = Date.now();
10778
- socket.on("message", (data, isBinary) => {
10779
- const now = Date.now();
10780
- tokens = Math.min(RELAY_BURST, tokens + (now - refilled) / 1e3 * RELAY_MESSAGES_PER_SECOND);
10781
- refilled = now;
10782
- if ((tokens -= 1) < 0) {
10783
- socket.close(1008, "too many messages");
10784
- return;
10785
- }
10786
- if (isBinary) return;
10787
- let message;
10788
- try {
10789
- message = JSON.parse(String(data));
10790
- } catch {
10791
- return;
10792
- }
10793
- if (typeof message?.type !== "string") return;
10794
- if (message.type === "join") {
10795
- if (client.did || typeof message.from !== "string" || !DID_PATTERN.test(message.from)) return;
10796
- if ([...rooms.get(room)].some((peer) => peer.did === message.from)) {
10797
- socket.close(4009, "that DID is already in this room");
10798
- return;
10799
- }
10800
- client.did = message.from;
10801
- broadcast(client, JSON.stringify({ type: "join", from: client.did }));
10802
- return;
10803
- }
10804
- if (!client.did || !["offer", "answer", "candidate"].includes(message.type) || typeof message.to !== "string") return;
10805
- const target = [...rooms.get(room) ?? []].find((peer) => peer.did === message.to);
10806
- const forwarded = { type: message.type, from: client.did, to: message.to, payload: message.payload };
10807
- if (target && target.socket.readyState === target.socket.OPEN) target.socket.send(JSON.stringify(forwarded));
10808
- });
10809
- socket.on("close", () => {
10810
- const peers = rooms.get(room);
10811
- if (!peers?.delete(client)) return;
10812
- if (peers.size === 0) rooms.delete(room);
10813
- if (client.did) broadcast(client, JSON.stringify({ type: "leave", from: client.did }));
10814
- });
10815
- }
10816
- };
10817
- }
10949
+ var DID_PATTERN2 = /^did:key:z[1-9A-HJ-NP-Za-km-z]{1,250}$/;
10818
10950
  function parseHello(data, isBinary) {
10819
10951
  if (isBinary) return null;
10820
10952
  try {
10821
10953
  const hello = JSON.parse(String(data));
10822
- return hello.type === "hello" && typeof hello.did === "string" && DID_PATTERN.test(hello.did) && typeof hello.nonce === "string" ? { did: hello.did, nonce: hello.nonce, proof: { sig: hello.sig, read: hello.read } } : null;
10954
+ return hello.type === "hello" && typeof hello.did === "string" && DID_PATTERN2.test(hello.did) && typeof hello.nonce === "string" ? { did: hello.did, nonce: hello.nonce, proof: { sig: hello.sig, read: hello.read } } : null;
10823
10955
  } catch {
10824
10956
  return null;
10825
10957
  }
10826
10958
  }
10827
10959
  async function serve(options) {
10828
10960
  const { node, inbound } = options;
10829
- const relay = createRelay();
10961
+ const relay = createRelay({ turn: turnFromEnv(process.env) });
10830
10962
  const sockets = new WebSocketServer({ noServer: true, maxPayload: 16 * 1024 * 1024 });
10831
- const relaySockets = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
10963
+ const relaySockets = new WebSocketServer({ noServer: true, maxPayload: MAX_MESSAGE_BYTES, perMessageDeflate: false, clientTracking: false });
10832
10964
  const http = createServer((req, res) => {
10833
10965
  if (req.url === "/health") {
10834
10966
  res.writeHead(200, { "content-type": "application/json" });
@@ -10876,7 +11008,7 @@ async function serve(options) {
10876
11008
  if (url.pathname === "/peer") {
10877
11009
  sockets.handleUpgrade(req, socket, head, (ws) => void onPeer(ws, url).catch(() => ws.close(1011, "internal error")));
10878
11010
  } else {
10879
- relaySockets.handleUpgrade(req, socket, head, (ws) => relay.accept(ws, url.searchParams.get("room") ?? "default"));
11011
+ relay.upgrade(relaySockets, req, socket, head);
10880
11012
  }
10881
11013
  });
10882
11014
  await new Promise((resolve, reject) => {
@@ -10888,7 +11020,8 @@ async function serve(options) {
10888
11020
  return {
10889
11021
  port,
10890
11022
  async close() {
10891
- for (const client of [...sockets.clients, ...relaySockets.clients]) client.terminate();
11023
+ for (const client of sockets.clients) client.terminate();
11024
+ relay.close();
10892
11025
  sockets.close();
10893
11026
  relaySockets.close();
10894
11027
  await new Promise((resolve) => http.close(() => resolve()));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weaveprotocol/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "The Weave protocol from a terminal: manage spaces, run an always-on node, serve MCP to agents",
5
5
  "type": "module",
6
6
  "bin": {