freertc 0.1.12 → 0.1.13

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 CHANGED
@@ -216,7 +216,7 @@ Quick checks:
216
216
  Expected `/health` response includes JSON like:
217
217
 
218
218
  ```json
219
- {"ok":true,"version":"0.1.12","protocol_version":"1.0","peers":0}
219
+ {"ok":true,"version":"0.1.13","protocol_version":"1.0","peers":0}
220
220
  ```
221
221
 
222
222
  ## Auto WebRTC two-tab test
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freertc",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Cloudflare Worker signaling relay for WebRTC peers with D1 storage.",
5
5
  "keywords": [
6
6
  "webrtc",
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
7
7
  import { WebSocketServer } from 'ws';
8
8
 
9
9
  const PSP_VERSION = '1.0';
10
- const WORKER_VERSION = '0.1.12';
10
+ const WORKER_VERSION = '0.1.13';
11
11
  const DEFAULT_TTL_MS = 30_000;
12
12
  const MAX_TTL_MS = 120_000;
13
13
  const MAX_MESSAGE_SIZE = 64 * 1024;
package/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const PSP_VERSION = "1.0";
2
- const WORKER_VERSION = "0.1.12";
2
+ const WORKER_VERSION = "0.1.13";
3
3
 
4
4
  const DISCOVERY_TYPES = new Set(["announce", "withdraw", "discover", "peer_list", "redirect"]);
5
5
  const NEGOTIATION_TYPES = new Set(["connect_request", "connect_accept", "connect_reject", "offer", "answer", "ice_candidate", "ice_end", "renegotiate"]);
@@ -83,6 +83,20 @@ export default {
83
83
  return jsonResponse({ ok: false, error: "Method not allowed" }, 405);
84
84
  }
85
85
 
86
+ if (url.pathname === "/api/v1/peers") {
87
+ if (request.method === "GET") {
88
+ return handleListPeers(request, env);
89
+ }
90
+ return jsonResponse({ ok: false, error: "Method not allowed" }, 405);
91
+ }
92
+
93
+ if (url.pathname === "/api/v1/relay") {
94
+ if (request.method === "POST") {
95
+ return handleRelayForward(request, env);
96
+ }
97
+ return jsonResponse({ ok: false, error: "Method not allowed" }, 405);
98
+ }
99
+
86
100
  return env.ASSETS?.fetch(request) ?? new Response("Not Found", { status: 404 });
87
101
  }
88
102
  };
@@ -118,6 +132,51 @@ async function handleRegisterRelay(request, env) {
118
132
  return jsonResponse({ ok: true, relays });
119
133
  }
120
134
 
135
+ async function handleListPeers(request, env) {
136
+ if (!env.DB) return jsonResponse({ ok: false, error: "No database" }, 503);
137
+ const url = new URL(request.url);
138
+ const network = (url.searchParams.get("network") || "").trim();
139
+ const excludePeerId = (url.searchParams.get("exclude") || "").trim();
140
+ if (!network) return jsonResponse({ ok: false, error: "Missing network" }, 400);
141
+
142
+ const peers = await findPeers(env.DB, network, excludePeerId);
143
+ return jsonResponse({ ok: true, peers });
144
+ }
145
+
146
+ async function handleRelayForward(request, env) {
147
+ if (!env.DB) return jsonResponse({ ok: false, error: "No database" }, 503);
148
+
149
+ let body;
150
+ try {
151
+ body = await request.json();
152
+ } catch {
153
+ return jsonResponse({ ok: false, error: "Invalid JSON" }, 400);
154
+ }
155
+
156
+ const message = body?.message || body;
157
+ if (!validEnvelope(message)) {
158
+ return jsonResponse({ ok: false, error: "Invalid PSP envelope" }, 400);
159
+ }
160
+ if (!RELAY_TYPES.has(message.type)) {
161
+ return jsonResponse({ ok: false, error: "Unsupported relay message type" }, 400);
162
+ }
163
+ if (!message.to || typeof message.to !== "string") {
164
+ return jsonResponse({ ok: false, error: "Missing destination peer" }, 400);
165
+ }
166
+
167
+ const liveKey = `${message.network}:${message.to}`;
168
+ const live = livePeers.get(liveKey);
169
+ if (live) {
170
+ try {
171
+ live.socket.send(JSON.stringify(message));
172
+ return jsonResponse({ ok: true, delivered: true }, 200);
173
+ } catch {}
174
+ }
175
+
176
+ await insertRelayMessage(env.DB, message);
177
+ return jsonResponse({ ok: true, delivered: false, queued: true }, 202);
178
+ }
179
+
121
180
  // POST to the global hub; cache returned relay list into own D1 so both sides know each other
122
181
  async function registerWithHub(env, selfUrl) {
123
182
  const resp = await fetch(`${relayHttpBase(normalizeRelayUrl(env.GLOBAL_RELAY_URL))}/api/v1/relays`, {
@@ -162,106 +221,30 @@ function relayHttpBase(wsUrl) {
162
221
  return wsUrl.replace(/^wss?:\/\//, (m) => m === "wss://" ? "https://" : "http://").replace(/\/ws$/, "");
163
222
  }
164
223
 
165
- // Open a short-lived WebSocket to a remote relay: get its peer list and exchange relay lists
166
- async function queryRelayForPeers(relayUrl, network, selfRelayId, db, selfKnownRelays) {
224
+ // Query peer list from a remote relay over HTTP.
225
+ async function queryRelayForPeers(relayUrl, network, requesterPeerId) {
167
226
  try {
168
- const resp = await fetch(relayUrl, { headers: { Upgrade: "websocket" } });
169
- if (resp.status !== 101) return [];
170
- const ws = resp.webSocket;
171
- ws.accept();
172
-
173
- return await new Promise((resolve) => {
174
- const timer = setTimeout(() => { try { ws.close(); } catch {} resolve([]); }, 4000);
175
- let gotPeerList = false;
176
- const relayPeerId = selfRelayId || "relay-bridge";
177
-
178
- ws.addEventListener("message", async (ev) => {
179
- try {
180
- const msg = JSON.parse(ev.data);
181
- // Ignore broadcast peer_list updates triggered by announce heartbeats.
182
- // We only want the direct discover response addressed to this bridge peer.
183
- if (
184
- msg.type === "peer_list" &&
185
- msg.network === network &&
186
- msg.to === relayPeerId &&
187
- !gotPeerList
188
- ) {
189
- gotPeerList = true;
190
- clearTimeout(timer);
191
- ws.close();
192
- resolve((msg.body?.peers || []).map(p => ({ ...p, relay_url: relayUrl })));
193
- }
194
- // Cache any relay list the remote sends us via ext
195
- if (msg.type === "ext" && msg.body?.action === "relay_list" && db) {
196
- const remoteRelays = msg.body.relays || [];
197
- await Promise.all(
198
- remoteRelays.map(r => r.url ? upsertRelay(db, r.url, r.name || null).catch(() => {}) : null)
199
- );
200
- }
201
- } catch {}
202
- });
203
-
204
- ws.addEventListener("error", () => { clearTimeout(timer); resolve([]); });
205
- ws.addEventListener("close", () => { if (!gotPeerList) { clearTimeout(timer); resolve([]); } });
206
-
207
- // Announce as relay bridge
208
- ws.send(JSON.stringify({
209
- psp_version: PSP_VERSION, type: "announce", network,
210
- from: relayPeerId, message_id: crypto.randomUUID(),
211
- timestamp: Date.now(), ttl_ms: 10_000, body: { capabilities: { relay: true } }
212
- }));
213
- // Share our known relay list so the remote can cache us
214
- if (selfKnownRelays?.length) {
215
- ws.send(JSON.stringify({
216
- psp_version: PSP_VERSION, type: "ext", network,
217
- from: relayPeerId, message_id: crypto.randomUUID(),
218
- timestamp: Date.now(), ttl_ms: 10_000,
219
- body: { action: "relay_list", relays: selfKnownRelays }
220
- }));
221
- }
222
- // Request their peers
223
- ws.send(JSON.stringify({
224
- psp_version: PSP_VERSION, type: "discover", network,
225
- from: relayPeerId, message_id: crypto.randomUUID(),
226
- timestamp: Date.now(), ttl_ms: 10_000, body: {}
227
- }));
228
- });
227
+ const base = relayHttpBase(relayUrl);
228
+ const params = new URLSearchParams({ network });
229
+ if (requesterPeerId) params.set("exclude", requesterPeerId);
230
+ const resp = await fetch(`${base}/api/v1/peers?${params.toString()}`);
231
+ if (!resp.ok) return [];
232
+ const data = await resp.json();
233
+ const peers = Array.isArray(data?.peers) ? data.peers : [];
234
+ return peers.map(p => ({ ...p, relay_url: relayUrl }));
229
235
  } catch {
230
236
  return [];
231
237
  }
232
238
  }
233
239
 
234
- // Open a short-lived WebSocket to a remote relay and forward a PSP message through it
240
+ // Forward a PSP message to a remote relay over HTTP for delivery or queueing.
235
241
  async function forwardToRelay(relayUrl, message, selfRelayId) {
236
242
  try {
237
- const wsUrl = relayUrl;
238
- const resp = await fetch(wsUrl, { headers: { Upgrade: "websocket" } });
239
- if (resp.status !== 101) return;
240
- const ws = resp.webSocket;
241
- ws.accept();
242
-
243
- await new Promise((resolve) => {
244
- const relayPeerId = selfRelayId || "relay-bridge";
245
- const closeTimer = setTimeout(() => {
246
- try { ws.close(); } catch {}
247
- resolve();
248
- }, 250);
249
-
250
- const finish = () => {
251
- clearTimeout(closeTimer);
252
- resolve();
253
- };
254
-
255
- ws.addEventListener("error", finish, { once: true });
256
- ws.addEventListener("close", finish, { once: true });
257
-
258
- // Outbound Worker WebSocket: send immediately after accept(), no open event needed.
259
- ws.send(JSON.stringify({
260
- psp_version: PSP_VERSION, type: "announce", network: message.network,
261
- from: relayPeerId, message_id: crypto.randomUUID(),
262
- timestamp: Date.now(), ttl_ms: 10_000, body: { capabilities: { relay: true } }
263
- }));
264
- ws.send(JSON.stringify(message));
243
+ const base = relayHttpBase(relayUrl);
244
+ await fetch(`${base}/api/v1/relay`, {
245
+ method: "POST",
246
+ headers: { "Content-Type": "application/json" },
247
+ body: JSON.stringify({ message, via: selfRelayId || "relay-bridge" })
265
248
  });
266
249
  } catch {}
267
250
  }
@@ -621,14 +604,13 @@ async function handleClientMessage(socket, rawData, env, ctx, prevPeerKey = null
621
604
 
622
605
  let remotePeers = [];
623
606
  if (env.RELAY_URL && env.DB) {
624
- const selfRelayId = env.RELAY_PEER_ID || "relay-bridge";
625
607
  const selfUrl = normalizeRelayUrl(env.RELAY_URL);
626
608
  const allRelays = await listRelays(env.DB);
627
609
  const remoteUrls = allRelays.map(r => r.url).filter(u => u !== selfUrl);
628
610
 
629
611
  if (remoteUrls.length) {
630
612
  const results = await Promise.all(
631
- remoteUrls.map(u => queryRelayForPeers(u, network, selfRelayId, env.DB, allRelays))
613
+ remoteUrls.map(u => queryRelayForPeers(u, network, peerId))
632
614
  );
633
615
  remotePeers = results.flat();
634
616
  }