@weaveprotocol/cli 0.1.0 → 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 +362 -88
- 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
|
@@ -6583,6 +6583,19 @@ function createEmitter() {
|
|
|
6583
6583
|
|
|
6584
6584
|
// ../src/network/signaling.ts
|
|
6585
6585
|
var SIGNAL_KINDS = /* @__PURE__ */ new Set(["offer", "answer", "candidate"]);
|
|
6586
|
+
function iceFrom(payload) {
|
|
6587
|
+
const { servers, expiresAt } = payload ?? {};
|
|
6588
|
+
if (!Array.isArray(servers) || typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return null;
|
|
6589
|
+
const kept = [];
|
|
6590
|
+
for (const server of servers.slice(0, 4)) {
|
|
6591
|
+
const urls = (Array.isArray(server?.urls) ? server.urls : [server?.urls]).filter(
|
|
6592
|
+
(url) => typeof url === "string" && url.length < 256 && /^turns?:/.test(url)
|
|
6593
|
+
);
|
|
6594
|
+
if (urls.length === 0 || typeof server.username !== "string" || typeof server.credential !== "string") continue;
|
|
6595
|
+
kept.push({ urls, username: server.username, credential: server.credential });
|
|
6596
|
+
}
|
|
6597
|
+
return kept.length > 0 ? { servers: kept, expiresAt } : null;
|
|
6598
|
+
}
|
|
6586
6599
|
function createSignalingClient(url, did) {
|
|
6587
6600
|
let ws = null;
|
|
6588
6601
|
let connected = false;
|
|
@@ -6619,7 +6632,10 @@ function createSignalingClient(url, did) {
|
|
|
6619
6632
|
try {
|
|
6620
6633
|
const msg = JSON.parse(event.data);
|
|
6621
6634
|
if (SIGNAL_KINDS.has(msg.type)) emit("signal", msg);
|
|
6622
|
-
else if (
|
|
6635
|
+
else if (msg.type === "ice") {
|
|
6636
|
+
const offered2 = iceFrom(msg.payload);
|
|
6637
|
+
if (offered2) emit("ice", offered2.servers, offered2.expiresAt);
|
|
6638
|
+
} else if (typeof msg.room !== "string") return;
|
|
6623
6639
|
else if (msg.type === "join") emit("peer-joined", msg.from, msg.room);
|
|
6624
6640
|
else if (msg.type === "leave") emit("peer-left", msg.from, msg.room);
|
|
6625
6641
|
} catch (err2) {
|
|
@@ -6675,6 +6691,7 @@ function createSignalingClient(url, did) {
|
|
|
6675
6691
|
if (rooms.delete(room)) sendMessage({ type: "leave", room });
|
|
6676
6692
|
},
|
|
6677
6693
|
signal: (kind, targetDid, payload) => sendMessage({ type: kind, to: targetDid, payload }),
|
|
6694
|
+
requestIce: () => sendMessage({ type: "ice" }),
|
|
6678
6695
|
on,
|
|
6679
6696
|
off,
|
|
6680
6697
|
isConnected: () => connected
|
|
@@ -6690,6 +6707,7 @@ function createMultiSignalingClient(urls, did) {
|
|
|
6690
6707
|
const { on, off, emit } = createEmitter();
|
|
6691
6708
|
const routes = /* @__PURE__ */ new Map();
|
|
6692
6709
|
const presence = /* @__PURE__ */ new Map();
|
|
6710
|
+
const turn = /* @__PURE__ */ new Map();
|
|
6693
6711
|
const connectedCount = () => clients.filter((client) => client.isConnected()).length;
|
|
6694
6712
|
const remember = (peer, client) => {
|
|
6695
6713
|
(routes.get(peer) ?? routes.set(peer, /* @__PURE__ */ new Set()).get(peer)).add(client);
|
|
@@ -6718,6 +6736,11 @@ function createMultiSignalingClient(urls, did) {
|
|
|
6718
6736
|
remember(message.from, client);
|
|
6719
6737
|
emit("signal", message);
|
|
6720
6738
|
});
|
|
6739
|
+
client.on("ice", (servers, expiresAt) => {
|
|
6740
|
+
turn.set(client, { servers, expiresAt });
|
|
6741
|
+
const offers = [...turn.values()];
|
|
6742
|
+
emit("ice", offers.flatMap((offer) => offer.servers), Math.min(...offers.map((offer) => offer.expiresAt)));
|
|
6743
|
+
});
|
|
6721
6744
|
client.on("connected", () => {
|
|
6722
6745
|
if (connectedCount() === 1) emit("connected");
|
|
6723
6746
|
});
|
|
@@ -6740,6 +6763,7 @@ function createMultiSignalingClient(urls, did) {
|
|
|
6740
6763
|
for (const client of clients) client.disconnect();
|
|
6741
6764
|
routes.clear();
|
|
6742
6765
|
presence.clear();
|
|
6766
|
+
turn.clear();
|
|
6743
6767
|
},
|
|
6744
6768
|
join: (room) => {
|
|
6745
6769
|
for (const client of clients) client.join(room);
|
|
@@ -6751,6 +6775,9 @@ function createMultiSignalingClient(urls, did) {
|
|
|
6751
6775
|
signal: (kind, target, payload) => {
|
|
6752
6776
|
for (const client of routesFor(target)) client.signal(kind, target, payload);
|
|
6753
6777
|
},
|
|
6778
|
+
requestIce: () => {
|
|
6779
|
+
for (const client of clients) if (client.isConnected()) client.requestIce();
|
|
6780
|
+
},
|
|
6754
6781
|
on,
|
|
6755
6782
|
off,
|
|
6756
6783
|
isConnected: () => connectedCount() > 0
|
|
@@ -6806,7 +6833,8 @@ var DEFAULT_ICE_SERVERS = [
|
|
|
6806
6833
|
{ urls: "stun:stun1.l.google.com:19302" }
|
|
6807
6834
|
];
|
|
6808
6835
|
function createRTCTransport(config) {
|
|
6809
|
-
const
|
|
6836
|
+
const configured = config?.iceServers ?? DEFAULT_ICE_SERVERS;
|
|
6837
|
+
const iceServers = () => typeof configured === "function" ? configured() : configured;
|
|
6810
6838
|
const connections = /* @__PURE__ */ new Map();
|
|
6811
6839
|
const { on, off, emit } = createEmitter();
|
|
6812
6840
|
const setupDataChannel = (peerId, channel) => {
|
|
@@ -6834,7 +6862,7 @@ function createRTCTransport(config) {
|
|
|
6834
6862
|
if (connections.has(peerId)) {
|
|
6835
6863
|
close(peerId);
|
|
6836
6864
|
}
|
|
6837
|
-
const connection = new RTCPeerConnection({ iceServers: [...iceServers] });
|
|
6865
|
+
const connection = new RTCPeerConnection({ iceServers: [...iceServers()] });
|
|
6838
6866
|
connections.set(peerId, { connection, channel: null });
|
|
6839
6867
|
connection.onicecandidate = (event) => {
|
|
6840
6868
|
if (event.candidate) onCandidate(event.candidate.toJSON());
|
|
@@ -7137,10 +7165,20 @@ function createWebSocketTransport(config) {
|
|
|
7137
7165
|
}
|
|
7138
7166
|
|
|
7139
7167
|
// ../src/network/mesh.ts
|
|
7168
|
+
var ICE_REFRESH_MS = 10 * 6e4;
|
|
7169
|
+
var ICE_WAIT_MS = 1500;
|
|
7140
7170
|
function createMesh(config) {
|
|
7141
7171
|
const { did } = config;
|
|
7142
|
-
const transport = config.createTransport?.() ?? createRTCTransport({ iceServers: config.iceServers });
|
|
7143
7172
|
const signaling = createMultiSignalingClient(config.relays, did);
|
|
7173
|
+
const configuredIce = config.iceServers ?? DEFAULT_ICE_SERVERS;
|
|
7174
|
+
let relayIce = null;
|
|
7175
|
+
let iceAskedAt = 0;
|
|
7176
|
+
signaling.on("ice", (servers, expiresAt) => {
|
|
7177
|
+
relayIce = { servers, expiresAt };
|
|
7178
|
+
iceAskedAt = 0;
|
|
7179
|
+
});
|
|
7180
|
+
const currentIce = () => relayIce && relayIce.expiresAt > Date.now() ? [...configuredIce, ...relayIce.servers] : configuredIce;
|
|
7181
|
+
const transport = config.createTransport?.() ?? createRTCTransport({ iceServers: currentIce });
|
|
7144
7182
|
const introduce = config.introductions !== false;
|
|
7145
7183
|
const authTimeoutMs = config.authTimeoutMs ?? 1e4;
|
|
7146
7184
|
const rooms = /* @__PURE__ */ new Map();
|
|
@@ -7341,6 +7379,23 @@ function createMesh(config) {
|
|
|
7341
7379
|
return started;
|
|
7342
7380
|
};
|
|
7343
7381
|
return {
|
|
7382
|
+
async iceServers() {
|
|
7383
|
+
const stale = !relayIce || relayIce.expiresAt - Date.now() < ICE_REFRESH_MS;
|
|
7384
|
+
if (signaling.isConnected() && stale && Date.now() - iceAskedAt > ICE_REFRESH_MS) {
|
|
7385
|
+
iceAskedAt = Date.now();
|
|
7386
|
+
await new Promise((resolve) => {
|
|
7387
|
+
const done = () => {
|
|
7388
|
+
clearTimeout(timer);
|
|
7389
|
+
signaling.off("ice", done);
|
|
7390
|
+
resolve();
|
|
7391
|
+
};
|
|
7392
|
+
const timer = setTimeout(done, ICE_WAIT_MS);
|
|
7393
|
+
signaling.on("ice", done);
|
|
7394
|
+
signaling.requestIce();
|
|
7395
|
+
});
|
|
7396
|
+
}
|
|
7397
|
+
return currentIce();
|
|
7398
|
+
},
|
|
7344
7399
|
join(name, auth = null) {
|
|
7345
7400
|
const room = { auth, peers: /* @__PURE__ */ new Map(), handshakes: /* @__PURE__ */ new Map(), events: createEmitter() };
|
|
7346
7401
|
const send = (peer, message) => {
|
|
@@ -8156,6 +8211,13 @@ var writeCapability = (spaceId) => ({
|
|
|
8156
8211
|
can: "expression/write"
|
|
8157
8212
|
});
|
|
8158
8213
|
var noteCid = (encoded) => cidFromBytes(utf8Encode(encoded));
|
|
8214
|
+
var NO_ACCOUNT = Object.freeze({ account: null, agent: false });
|
|
8215
|
+
var WHO_MESSAGE = "who";
|
|
8216
|
+
var LIVE_MESSAGE = "live";
|
|
8217
|
+
var MAX_LIVE_BYTES = 64 * 1024;
|
|
8218
|
+
var MAX_NOTE_LENGTH = 16 * 1024;
|
|
8219
|
+
var LIVE_BURST = 60;
|
|
8220
|
+
var LIVE_PER_SECOND = 20;
|
|
8159
8221
|
var STANDS = { ok: true };
|
|
8160
8222
|
function looksEncrypted(body) {
|
|
8161
8223
|
const envelope = body;
|
|
@@ -8657,11 +8719,45 @@ async function openSpaceRuntime(deps) {
|
|
|
8657
8719
|
}
|
|
8658
8720
|
}
|
|
8659
8721
|
const connectedPeers = () => [...new Set(networks.flatMap((network) => network.getPeers().map((peer) => peer.did)))];
|
|
8722
|
+
const peerAccounts = /* @__PURE__ */ new Map();
|
|
8723
|
+
const knownAccounts = /* @__PURE__ */ new Map();
|
|
8724
|
+
const liveAllowance = /* @__PURE__ */ new Map();
|
|
8725
|
+
async function accountOf(peer, note) {
|
|
8726
|
+
if (typeof note !== "string" || note.length > MAX_NOTE_LENGTH) return NO_ACCOUNT;
|
|
8727
|
+
const chain = await resolveDelegationRoot(note, () => null, provider).catch(() => null);
|
|
8728
|
+
if (!chain?.valid || chain.audience !== peer || !chain.rootDid) return NO_ACCOUNT;
|
|
8729
|
+
return { account: chain.rootDid, agent: isAgentNote(note) };
|
|
8730
|
+
}
|
|
8731
|
+
function withinAllowance(peer) {
|
|
8732
|
+
const now = Date.now();
|
|
8733
|
+
const held = liveAllowance.get(peer) ?? { tokens: LIVE_BURST, at: now };
|
|
8734
|
+
const tokens = Math.min(LIVE_BURST, held.tokens + (now - held.at) / 1e3 * LIVE_PER_SECOND);
|
|
8735
|
+
liveAllowance.set(peer, { tokens: Math.max(0, tokens - 1), at: now });
|
|
8736
|
+
return tokens >= 1;
|
|
8737
|
+
}
|
|
8738
|
+
async function onLive(peer, payload) {
|
|
8739
|
+
if (!withinAllowance(peer)) return;
|
|
8740
|
+
if ((JSON.stringify(payload ?? null)?.length ?? 0) > MAX_LIVE_BYTES) return;
|
|
8741
|
+
const { account, agent } = await peerAccounts.get(peer) ?? NO_ACCOUNT;
|
|
8742
|
+
if (!routes.has(peer)) return;
|
|
8743
|
+
emit({ type: "message", space: space2.id, from: account, peer, agent, message: payload });
|
|
8744
|
+
}
|
|
8660
8745
|
for (const network of networks) {
|
|
8661
8746
|
network.on("message", (message) => {
|
|
8662
8747
|
if (message.type === "sync") void sync.handleMessage(message.from, message.payload);
|
|
8748
|
+
else if (message.type === WHO_MESSAGE) {
|
|
8749
|
+
const note = message.payload?.note;
|
|
8750
|
+
const checked = accountOf(message.from, note);
|
|
8751
|
+
peerAccounts.set(message.from, checked);
|
|
8752
|
+
void checked.then((known) => {
|
|
8753
|
+
if (peerAccounts.get(message.from) !== checked) return;
|
|
8754
|
+
knownAccounts.set(message.from, known);
|
|
8755
|
+
emit({ type: "status", space: space2.id });
|
|
8756
|
+
});
|
|
8757
|
+
} else if (message.type === LIVE_MESSAGE) void onLive(message.from, message.payload);
|
|
8663
8758
|
});
|
|
8664
8759
|
network.on("peer-connected", (info) => {
|
|
8760
|
+
network.send(info.did, { type: WHO_MESSAGE, from: session.did, payload: { note: session.proof() } });
|
|
8665
8761
|
routes.set(info.did, network);
|
|
8666
8762
|
sync.addPeer(info.did);
|
|
8667
8763
|
sync.notifyPeers([info.did]);
|
|
@@ -8671,6 +8767,9 @@ async function openSpaceRuntime(deps) {
|
|
|
8671
8767
|
if (routes.get(info.did) === network) {
|
|
8672
8768
|
routes.delete(info.did);
|
|
8673
8769
|
sync.removePeer(info.did);
|
|
8770
|
+
peerAccounts.delete(info.did);
|
|
8771
|
+
knownAccounts.delete(info.did);
|
|
8772
|
+
liveAllowance.delete(info.did);
|
|
8674
8773
|
}
|
|
8675
8774
|
emit({ type: "status", space: space2.id });
|
|
8676
8775
|
});
|
|
@@ -8681,10 +8780,16 @@ async function openSpaceRuntime(deps) {
|
|
|
8681
8780
|
}
|
|
8682
8781
|
if (networks.length > 0) {
|
|
8683
8782
|
connection = "connecting";
|
|
8684
|
-
void Promise.
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8783
|
+
void Promise.any(networks.map((network) => network.connect())).then(
|
|
8784
|
+
() => {
|
|
8785
|
+
connection = "connected";
|
|
8786
|
+
emit({ type: "status", space: space2.id });
|
|
8787
|
+
},
|
|
8788
|
+
() => {
|
|
8789
|
+
connection = "error";
|
|
8790
|
+
emit({ type: "status", space: space2.id });
|
|
8791
|
+
}
|
|
8792
|
+
);
|
|
8688
8793
|
sync.start();
|
|
8689
8794
|
}
|
|
8690
8795
|
let watchTimer = null;
|
|
@@ -9031,10 +9136,19 @@ async function openSpaceRuntime(deps) {
|
|
|
9031
9136
|
space: space2.id,
|
|
9032
9137
|
connection,
|
|
9033
9138
|
peers: connectedPeers(),
|
|
9139
|
+
accounts: Object.fromEntries([...knownAccounts].flatMap(([peer, known]) => known.account && routes.has(peer) ? [[peer, known.account]] : [])),
|
|
9034
9140
|
root: await storage.getRootCid(),
|
|
9035
9141
|
rejected
|
|
9036
9142
|
};
|
|
9037
9143
|
},
|
|
9144
|
+
async send(message, to) {
|
|
9145
|
+
const encoded = JSON.stringify(message ?? null);
|
|
9146
|
+
if (encoded.length > MAX_LIVE_BYTES) throw new Error("A live message can be at most 64 KB");
|
|
9147
|
+
for (const peer of connectedPeers()) {
|
|
9148
|
+
if (to && to !== peer && (await peerAccounts.get(peer))?.account !== to) continue;
|
|
9149
|
+
routes.get(peer)?.send(peer, { type: LIVE_MESSAGE, from: session.did, payload: message ?? null });
|
|
9150
|
+
}
|
|
9151
|
+
},
|
|
9038
9152
|
async close() {
|
|
9039
9153
|
if (watchTimer) clearInterval(watchTimer);
|
|
9040
9154
|
if (announceTimer) clearTimeout(announceTimer);
|
|
@@ -9114,6 +9228,7 @@ async function createNode(config) {
|
|
|
9114
9228
|
const registryStore = await config.stores("registry", { seal: true });
|
|
9115
9229
|
const registry = createSpaceManager(registryStore, provider);
|
|
9116
9230
|
const runtimes = /* @__PURE__ */ new Map();
|
|
9231
|
+
const holds2 = /* @__PURE__ */ new Map();
|
|
9117
9232
|
const account = config.accountKey ? await deriveAccountRegistry(config.accountKey, config.signer.did, provider) : null;
|
|
9118
9233
|
const accountSpaceId = account?.space.id ?? null;
|
|
9119
9234
|
async function findRecord(spaceId) {
|
|
@@ -9224,6 +9339,7 @@ async function createNode(config) {
|
|
|
9224
9339
|
}
|
|
9225
9340
|
}
|
|
9226
9341
|
async function closeRuntime(spaceId) {
|
|
9342
|
+
holds2.delete(spaceId);
|
|
9227
9343
|
const open2 = runtimes.get(spaceId);
|
|
9228
9344
|
if (!open2) return;
|
|
9229
9345
|
runtimes.delete(spaceId);
|
|
@@ -9406,10 +9522,28 @@ async function createNode(config) {
|
|
|
9406
9522
|
await registry.remove(spaceId);
|
|
9407
9523
|
emit({ type: "spaces" });
|
|
9408
9524
|
},
|
|
9409
|
-
async
|
|
9410
|
-
|
|
9525
|
+
async hold(spaceId) {
|
|
9526
|
+
const held = holds2.get(spaceId) ?? holds2.set(spaceId, { count: 0 }).get(spaceId);
|
|
9527
|
+
held.count += 1;
|
|
9528
|
+
let released = false;
|
|
9529
|
+
const release = async () => {
|
|
9530
|
+
if (released) return;
|
|
9531
|
+
released = true;
|
|
9532
|
+
if (holds2.get(spaceId) !== held) return;
|
|
9533
|
+
held.count -= 1;
|
|
9534
|
+
if (held.count === 0) await closeRuntime(spaceId);
|
|
9535
|
+
};
|
|
9536
|
+
try {
|
|
9537
|
+
await runtime(spaceId);
|
|
9538
|
+
} catch (error) {
|
|
9539
|
+
await release();
|
|
9540
|
+
throw error;
|
|
9541
|
+
}
|
|
9542
|
+
return release;
|
|
9543
|
+
},
|
|
9544
|
+
async send(spaceId, message, to) {
|
|
9545
|
+
await (await runtime(spaceId)).send(message, to);
|
|
9411
9546
|
},
|
|
9412
|
-
close: closeRuntime,
|
|
9413
9547
|
async status(spaceId) {
|
|
9414
9548
|
const status = await (await runtime(spaceId)).status();
|
|
9415
9549
|
if (!accountSpaceId) return { ...status, own: [], carriers: [] };
|
|
@@ -9618,9 +9752,11 @@ async function createNode(config) {
|
|
|
9618
9752
|
closeInvite: person("close invites"),
|
|
9619
9753
|
revoke: person("revoke notes"),
|
|
9620
9754
|
access: async (spaceId) => (inside(spaceId), spaces.access(spaceId)),
|
|
9621
|
-
|
|
9755
|
+
hold: async (spaceId) => (inside(spaceId), spaces.hold(spaceId)),
|
|
9622
9756
|
status: async (spaceId) => (inside(spaceId), spaces.status(spaceId)),
|
|
9623
9757
|
profiles: async (spaceId) => (inside(spaceId), spaces.profiles(spaceId)),
|
|
9758
|
+
// It would arrive as the person: a live message carries no note of its own to say "via agent".
|
|
9759
|
+
send: person("send live messages"),
|
|
9624
9760
|
authenticator: async () => null
|
|
9625
9761
|
});
|
|
9626
9762
|
const agentRecords = Object.freeze({
|
|
@@ -9670,6 +9806,7 @@ async function createNode(config) {
|
|
|
9670
9806
|
account: Object.freeze({ profile: accountApi.profile, setName: person("rename the account"), revoke: person("revoke notes") }),
|
|
9671
9807
|
carriers: Object.freeze({ list: carriers.list, add: person("add a carrier"), remove: person("remove a carrier") }),
|
|
9672
9808
|
delegation: () => note,
|
|
9809
|
+
iceServers: node.iceServers,
|
|
9673
9810
|
delegate: person("pass its access on"),
|
|
9674
9811
|
asAgent: person("start another agent"),
|
|
9675
9812
|
subscribe: (listener) => node.subscribe((event) => {
|
|
@@ -9690,6 +9827,7 @@ async function createNode(config) {
|
|
|
9690
9827
|
carriers,
|
|
9691
9828
|
asAgent,
|
|
9692
9829
|
delegation: () => current,
|
|
9830
|
+
iceServers: async () => mesh ? mesh.iceServers() : config.network?.iceServers ?? DEFAULT_ICE_SERVERS,
|
|
9693
9831
|
async delegate(params) {
|
|
9694
9832
|
const token = await delegateCapabilities(
|
|
9695
9833
|
{
|
|
@@ -10549,6 +10687,201 @@ async function unlock(home, account, secret) {
|
|
|
10549
10687
|
// src/serve.ts
|
|
10550
10688
|
import { createServer } from "node:http";
|
|
10551
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
|
|
10552
10885
|
function createSpacePeers() {
|
|
10553
10886
|
const sockets = /* @__PURE__ */ new Map();
|
|
10554
10887
|
const listeners = {};
|
|
@@ -10613,84 +10946,21 @@ function createInboundPeers() {
|
|
|
10613
10946
|
count: () => spaces.size
|
|
10614
10947
|
};
|
|
10615
10948
|
}
|
|
10616
|
-
var
|
|
10617
|
-
var RELAY_MAX_PEERS_PER_ROOM = 64;
|
|
10618
|
-
var RELAY_MESSAGES_PER_SECOND = 50;
|
|
10619
|
-
var RELAY_BURST = 100;
|
|
10620
|
-
var DID_PATTERN = /^did:key:z[1-9A-HJ-NP-Za-km-z]{1,250}$/;
|
|
10621
|
-
function createRelay() {
|
|
10622
|
-
const rooms = /* @__PURE__ */ new Map();
|
|
10623
|
-
const broadcast = (sender, payload) => {
|
|
10624
|
-
for (const peer of rooms.get(sender.room) ?? []) {
|
|
10625
|
-
if (peer !== sender && peer.did && peer.socket.readyState === peer.socket.OPEN) peer.socket.send(payload);
|
|
10626
|
-
}
|
|
10627
|
-
};
|
|
10628
|
-
return {
|
|
10629
|
-
rooms,
|
|
10630
|
-
accept(socket, room) {
|
|
10631
|
-
if (room.length > RELAY_MAX_ROOM_LENGTH || (rooms.get(room)?.size ?? 0) >= RELAY_MAX_PEERS_PER_ROOM) {
|
|
10632
|
-
socket.close(1008, "room refused");
|
|
10633
|
-
return;
|
|
10634
|
-
}
|
|
10635
|
-
const client = { socket, room, did: null };
|
|
10636
|
-
if (!rooms.has(room)) rooms.set(room, /* @__PURE__ */ new Set());
|
|
10637
|
-
rooms.get(room).add(client);
|
|
10638
|
-
let tokens = RELAY_BURST;
|
|
10639
|
-
let refilled = Date.now();
|
|
10640
|
-
socket.on("message", (data, isBinary) => {
|
|
10641
|
-
const now = Date.now();
|
|
10642
|
-
tokens = Math.min(RELAY_BURST, tokens + (now - refilled) / 1e3 * RELAY_MESSAGES_PER_SECOND);
|
|
10643
|
-
refilled = now;
|
|
10644
|
-
if ((tokens -= 1) < 0) {
|
|
10645
|
-
socket.close(1008, "too many messages");
|
|
10646
|
-
return;
|
|
10647
|
-
}
|
|
10648
|
-
if (isBinary) return;
|
|
10649
|
-
let message;
|
|
10650
|
-
try {
|
|
10651
|
-
message = JSON.parse(String(data));
|
|
10652
|
-
} catch {
|
|
10653
|
-
return;
|
|
10654
|
-
}
|
|
10655
|
-
if (typeof message?.type !== "string") return;
|
|
10656
|
-
if (message.type === "join") {
|
|
10657
|
-
if (client.did || typeof message.from !== "string" || !DID_PATTERN.test(message.from)) return;
|
|
10658
|
-
if ([...rooms.get(room)].some((peer) => peer.did === message.from)) {
|
|
10659
|
-
socket.close(4009, "that DID is already in this room");
|
|
10660
|
-
return;
|
|
10661
|
-
}
|
|
10662
|
-
client.did = message.from;
|
|
10663
|
-
broadcast(client, JSON.stringify({ type: "join", from: client.did }));
|
|
10664
|
-
return;
|
|
10665
|
-
}
|
|
10666
|
-
if (!client.did || !["offer", "answer", "candidate"].includes(message.type) || typeof message.to !== "string") return;
|
|
10667
|
-
const target = [...rooms.get(room) ?? []].find((peer) => peer.did === message.to);
|
|
10668
|
-
const forwarded = { type: message.type, from: client.did, to: message.to, payload: message.payload };
|
|
10669
|
-
if (target && target.socket.readyState === target.socket.OPEN) target.socket.send(JSON.stringify(forwarded));
|
|
10670
|
-
});
|
|
10671
|
-
socket.on("close", () => {
|
|
10672
|
-
const peers = rooms.get(room);
|
|
10673
|
-
if (!peers?.delete(client)) return;
|
|
10674
|
-
if (peers.size === 0) rooms.delete(room);
|
|
10675
|
-
if (client.did) broadcast(client, JSON.stringify({ type: "leave", from: client.did }));
|
|
10676
|
-
});
|
|
10677
|
-
}
|
|
10678
|
-
};
|
|
10679
|
-
}
|
|
10949
|
+
var DID_PATTERN2 = /^did:key:z[1-9A-HJ-NP-Za-km-z]{1,250}$/;
|
|
10680
10950
|
function parseHello(data, isBinary) {
|
|
10681
10951
|
if (isBinary) return null;
|
|
10682
10952
|
try {
|
|
10683
10953
|
const hello = JSON.parse(String(data));
|
|
10684
|
-
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;
|
|
10685
10955
|
} catch {
|
|
10686
10956
|
return null;
|
|
10687
10957
|
}
|
|
10688
10958
|
}
|
|
10689
10959
|
async function serve(options) {
|
|
10690
10960
|
const { node, inbound } = options;
|
|
10691
|
-
const relay = createRelay();
|
|
10961
|
+
const relay = createRelay({ turn: turnFromEnv(process.env) });
|
|
10692
10962
|
const sockets = new WebSocketServer({ noServer: true, maxPayload: 16 * 1024 * 1024 });
|
|
10693
|
-
const relaySockets = new WebSocketServer({ noServer: true, maxPayload:
|
|
10963
|
+
const relaySockets = new WebSocketServer({ noServer: true, maxPayload: MAX_MESSAGE_BYTES, perMessageDeflate: false, clientTracking: false });
|
|
10694
10964
|
const http = createServer((req, res) => {
|
|
10695
10965
|
if (req.url === "/health") {
|
|
10696
10966
|
res.writeHead(200, { "content-type": "application/json" });
|
|
@@ -10700,6 +10970,7 @@ async function serve(options) {
|
|
|
10700
10970
|
res.writeHead(426, { "content-type": "text/plain" });
|
|
10701
10971
|
res.end("This endpoint speaks WebSocket only.\n");
|
|
10702
10972
|
});
|
|
10973
|
+
const served = /* @__PURE__ */ new Set();
|
|
10703
10974
|
const onPeer = async (socket, url) => {
|
|
10704
10975
|
const spaceId = url.searchParams.get("space");
|
|
10705
10976
|
if (!spaceId) {
|
|
@@ -10721,7 +10992,10 @@ async function serve(options) {
|
|
|
10721
10992
|
socket.close(4003, "not a reader of this space");
|
|
10722
10993
|
return;
|
|
10723
10994
|
}
|
|
10724
|
-
|
|
10995
|
+
if (!served.has(spaceId)) {
|
|
10996
|
+
await node.spaces.hold(spaceId);
|
|
10997
|
+
served.add(spaceId);
|
|
10998
|
+
}
|
|
10725
10999
|
const sig = await authenticator.welcome(node.sessionDid, hello.nonce);
|
|
10726
11000
|
socket.send(JSON.stringify({ type: "welcome", did: node.sessionDid, sig }));
|
|
10727
11001
|
inbound.accept(spaceId, socket, hello.did);
|
|
@@ -10734,7 +11008,7 @@ async function serve(options) {
|
|
|
10734
11008
|
if (url.pathname === "/peer") {
|
|
10735
11009
|
sockets.handleUpgrade(req, socket, head, (ws) => void onPeer(ws, url).catch(() => ws.close(1011, "internal error")));
|
|
10736
11010
|
} else {
|
|
10737
|
-
|
|
11011
|
+
relay.upgrade(relaySockets, req, socket, head);
|
|
10738
11012
|
}
|
|
10739
11013
|
});
|
|
10740
11014
|
await new Promise((resolve, reject) => {
|
|
@@ -10746,7 +11020,8 @@ async function serve(options) {
|
|
|
10746
11020
|
return {
|
|
10747
11021
|
port,
|
|
10748
11022
|
async close() {
|
|
10749
|
-
for (const client of
|
|
11023
|
+
for (const client of sockets.clients) client.terminate();
|
|
11024
|
+
relay.close();
|
|
10750
11025
|
sockets.close();
|
|
10751
11026
|
relaySockets.close();
|
|
10752
11027
|
await new Promise((resolve) => http.close(() => resolve()));
|
|
@@ -10771,18 +11046,17 @@ async function startDaemon(options) {
|
|
|
10771
11046
|
...options.nodes?.length ? { nodes: options.nodes } : {}
|
|
10772
11047
|
}
|
|
10773
11048
|
});
|
|
10774
|
-
const open2 = /* @__PURE__ */ new
|
|
11049
|
+
const open2 = /* @__PURE__ */ new Map();
|
|
10775
11050
|
const rescan = async () => {
|
|
10776
11051
|
const held = new Set((await node.spaces.list()).map((space2) => space2.id));
|
|
10777
11052
|
for (const id of held) {
|
|
10778
11053
|
if (open2.has(id)) continue;
|
|
10779
|
-
await node.spaces.
|
|
10780
|
-
open2.add(id);
|
|
11054
|
+
open2.set(id, await node.spaces.hold(id));
|
|
10781
11055
|
log(`serving space ${id}`);
|
|
10782
11056
|
}
|
|
10783
|
-
for (const id of [...open2]) {
|
|
11057
|
+
for (const [id, release] of [...open2]) {
|
|
10784
11058
|
if (held.has(id)) continue;
|
|
10785
|
-
await
|
|
11059
|
+
await release();
|
|
10786
11060
|
open2.delete(id);
|
|
10787
11061
|
log(`stopped serving space ${id}`);
|
|
10788
11062
|
}
|
|
@@ -11145,7 +11419,7 @@ async function startAgentNode(home, options = {}) {
|
|
|
11145
11419
|
for (const space2 of grant.spaces) if (!held.has(space2.id)) await base.spaces.join(space2.invite).catch(() => {
|
|
11146
11420
|
});
|
|
11147
11421
|
const node = await base.asAgent({ keys: key2.keys, note: grant.token });
|
|
11148
|
-
for (const space2 of await node.spaces.list()) void node.spaces.
|
|
11422
|
+
for (const space2 of await node.spaces.list()) void node.spaces.hold(space2.id).catch(() => {
|
|
11149
11423
|
});
|
|
11150
11424
|
return {
|
|
11151
11425
|
node,
|