@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.
- package/README.md +1 -1
- package/dist/weave.mjs +202 -69
- 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: `
|
|
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
|
|
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" &&
|
|
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:
|
|
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
|
-
|
|
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
|
|
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()));
|