@weaveprotocol/cli 0.1.0 → 0.1.1

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 (2) hide show
  1. package/dist/weave.mjs +160 -19
  2. package/package.json +1 -1
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 (typeof msg.room !== "string") return;
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 iceServers = config?.iceServers ?? DEFAULT_ICE_SERVERS;
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.allSettled(networks.map((network) => network.connect())).then((results) => {
8685
- connection = results.some((result) => result.status === "fulfilled") ? "connected" : "error";
8686
- emit({ type: "status", space: space2.id });
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 open(spaceId) {
9410
- await runtime(spaceId);
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
- open: async (spaceId) => (inside(spaceId), spaces.open(spaceId)),
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
  {
@@ -10700,6 +10838,7 @@ async function serve(options) {
10700
10838
  res.writeHead(426, { "content-type": "text/plain" });
10701
10839
  res.end("This endpoint speaks WebSocket only.\n");
10702
10840
  });
10841
+ const served = /* @__PURE__ */ new Set();
10703
10842
  const onPeer = async (socket, url) => {
10704
10843
  const spaceId = url.searchParams.get("space");
10705
10844
  if (!spaceId) {
@@ -10721,7 +10860,10 @@ async function serve(options) {
10721
10860
  socket.close(4003, "not a reader of this space");
10722
10861
  return;
10723
10862
  }
10724
- await node.spaces.open(spaceId);
10863
+ if (!served.has(spaceId)) {
10864
+ await node.spaces.hold(spaceId);
10865
+ served.add(spaceId);
10866
+ }
10725
10867
  const sig = await authenticator.welcome(node.sessionDid, hello.nonce);
10726
10868
  socket.send(JSON.stringify({ type: "welcome", did: node.sessionDid, sig }));
10727
10869
  inbound.accept(spaceId, socket, hello.did);
@@ -10771,18 +10913,17 @@ async function startDaemon(options) {
10771
10913
  ...options.nodes?.length ? { nodes: options.nodes } : {}
10772
10914
  }
10773
10915
  });
10774
- const open2 = /* @__PURE__ */ new Set();
10916
+ const open2 = /* @__PURE__ */ new Map();
10775
10917
  const rescan = async () => {
10776
10918
  const held = new Set((await node.spaces.list()).map((space2) => space2.id));
10777
10919
  for (const id of held) {
10778
10920
  if (open2.has(id)) continue;
10779
- await node.spaces.open(id);
10780
- open2.add(id);
10921
+ open2.set(id, await node.spaces.hold(id));
10781
10922
  log(`serving space ${id}`);
10782
10923
  }
10783
- for (const id of [...open2]) {
10924
+ for (const [id, release] of [...open2]) {
10784
10925
  if (held.has(id)) continue;
10785
- await node.spaces.close(id);
10926
+ await release();
10786
10927
  open2.delete(id);
10787
10928
  log(`stopped serving space ${id}`);
10788
10929
  }
@@ -11145,7 +11286,7 @@ async function startAgentNode(home, options = {}) {
11145
11286
  for (const space2 of grant.spaces) if (!held.has(space2.id)) await base.spaces.join(space2.invite).catch(() => {
11146
11287
  });
11147
11288
  const node = await base.asAgent({ keys: key2.keys, note: grant.token });
11148
- for (const space2 of await node.spaces.list()) void node.spaces.open(space2.id).catch(() => {
11289
+ for (const space2 of await node.spaces.list()) void node.spaces.hold(space2.id).catch(() => {
11149
11290
  });
11150
11291
  return {
11151
11292
  node,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weaveprotocol/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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": {