freertc 0.1.23 → 0.1.28

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.23","protocol_version":"1.0","peers":0}
219
+ {"ok":true,"version":"0.1.28","protocol_version":"1.0","peers":0}
220
220
  ```
221
221
 
222
222
  ## Auto WebRTC two-tab test
package/bin/freertc.mjs CHANGED
@@ -76,6 +76,11 @@ function normalizeHost(value) {
76
76
  return host || null;
77
77
  }
78
78
 
79
+ function isPlaceholderHost(host) {
80
+ if (!host || typeof host !== 'string') return false;
81
+ return /your-domain\.example|your[-_]?domain/i.test(host);
82
+ }
83
+
79
84
  function sanitizeDomain(domain) {
80
85
  if (!domain || typeof domain !== 'string') return null;
81
86
  return domain
@@ -95,11 +100,46 @@ function patchJsoncVar(text, varName, value) {
95
100
  return text;
96
101
  }
97
102
 
103
+ function ensureRoutesForHost(configText, host) {
104
+ const normalizedHost = normalizeHost(host);
105
+ if (!normalizedHost || isPlaceholderHost(normalizedHost)) {
106
+ return configText;
107
+ }
108
+
109
+ const pattern = `${normalizedHost}/*`;
110
+ let text = configText;
111
+
112
+ if (/^\s*"routes"\s*:\s*\[/m.test(text)) {
113
+ text = text.replace(/^(\s*"pattern"\s*:\s*")([^"]*)(")/m, `$1${pattern}$3`);
114
+ if (/^\s*"zone_name"\s*:\s*"[^"]*"/m.test(text)) {
115
+ text = text.replace(/^(\s*"zone_name"\s*:\s*")([^"]*)(")/m, `$1${normalizedHost}$3`);
116
+ }
117
+ return text;
118
+ }
119
+
120
+ const routesBlock = [
121
+ ' "routes": [',
122
+ ' {',
123
+ ` "pattern": "${pattern}",`,
124
+ ` "zone_name": "${normalizedHost}"`,
125
+ ' }',
126
+ ' ]'
127
+ ].join('\n');
128
+
129
+ if (/\n\s*"env"\s*:\s*\{/.test(text)) {
130
+ return text.replace(/\n\s*"env"\s*:\s*\{/, `\n${routesBlock},\n\n "env": {`);
131
+ }
132
+
133
+ return text.replace(/\n}\s*$/, `\n${routesBlock}\n}`);
134
+ }
135
+
98
136
  function extractRouteHost(configText) {
99
- const routeMatch = configText.match(/"pattern"\s*:\s*"([^"]+)"/);
137
+ const routeMatch = configText.match(/^\s*"pattern"\s*:\s*"([^"]+)"/m);
100
138
  if (!routeMatch) return null;
101
139
  const pattern = routeMatch[1];
102
- return normalizeHost(pattern.split('/')[0]);
140
+ const host = normalizeHost(pattern.split('/')[0]);
141
+ if (!host || isPlaceholderHost(host)) return null;
142
+ return host;
103
143
  }
104
144
 
105
145
  function extractCurrentWorkerName(configText) {
@@ -109,15 +149,24 @@ function extractCurrentWorkerName(configText) {
109
149
 
110
150
  function extractDeployHost(configPath) {
111
151
  const text = fs.readFileSync(configPath, 'utf8');
112
- return extractRouteHost(text) || extractRelayUrlHost(text) || extractDatabaseDomainHost(text);
152
+ return extractGlobalRelayUrlHost(text) || extractRouteHost(text) || extractDatabaseDomainHost(text) || extractRelayUrlHost(text);
153
+ }
154
+
155
+ function extractGlobalRelayUrlHost(configText) {
156
+ const match = configText.match(/"GLOBAL_RELAY_URL"\s*:\s*"([^"]*)"/);
157
+ if (!match) return null;
158
+ const host = normalizeHost(match[1]);
159
+ if (!host || isPlaceholderHost(host)) return null;
160
+ return host;
113
161
  }
114
162
 
115
163
  function extractRelayUrlHost(configText) {
116
164
  const relayUrlMatch = configText.match(/"RELAY_URL"\s*:\s*"([^"]*)"/);
117
165
  if (!relayUrlMatch) return null;
118
166
  const raw = relayUrlMatch[1];
119
- if (/your-domain\.example|your[-_]?domain/i.test(raw)) return null;
120
- return normalizeHost(raw);
167
+ const host = normalizeHost(raw);
168
+ if (!host || isPlaceholderHost(host)) return null;
169
+ return host;
121
170
  }
122
171
 
123
172
  function extractDatabaseDomainHost(configText) {
@@ -134,8 +183,10 @@ function extractDatabaseDomainHost(configText) {
134
183
  function autoPatchWranglerConfig(configPath) {
135
184
  let text = fs.readFileSync(configPath, 'utf8');
136
185
 
186
+ const publicHost = extractGlobalRelayUrlHost(text) || extractDatabaseDomainHost(text) || extractRouteHost(text);
137
187
  const routeHost = extractRouteHost(text);
138
- const envDomainHost = normalizeHost(process.env.CF_DOMAIN || process.env.CLOUD_FLARE_DOMAIN || '');
188
+ const envDomainCandidate = normalizeHost(process.env.CF_DOMAIN || process.env.CLOUD_FLARE_DOMAIN || '');
189
+ const envDomainHost = envDomainCandidate && !isPlaceholderHost(envDomainCandidate) ? envDomainCandidate : null;
139
190
  const relayHost = routeHost || envDomainHost || extractRelayUrlHost(text) || extractDatabaseDomainHost(text);
140
191
 
141
192
  if (!relayHost) {
@@ -150,6 +201,7 @@ function autoPatchWranglerConfig(configPath) {
150
201
  text = patchJsoncVar(text, 'RELAY_URL', relayWsUrl);
151
202
  text = patchJsoncVar(text, 'RELAY_NAME', relayName);
152
203
  text = text.replace(/("database_name"\s*:\s*)"freertc-signal(?:-your[-_]?domain)?"/gi, `$1"${dbName}"`);
204
+ text = ensureRoutesForHost(text, publicHost || relayHost);
153
205
 
154
206
  if (text !== before) {
155
207
  fs.writeFileSync(configPath, text, 'utf8');
@@ -268,6 +320,31 @@ async function apiGetJson(url, token) {
268
320
  return payload.result;
269
321
  }
270
322
 
323
+ async function apiMutateJson(url, token, method, body) {
324
+ const response = await fetch(url, {
325
+ method,
326
+ headers: {
327
+ Authorization: `Bearer ${token}`,
328
+ Accept: 'application/json',
329
+ 'Content-Type': 'application/json'
330
+ },
331
+ body: JSON.stringify(body)
332
+ });
333
+
334
+ let payload;
335
+ try {
336
+ payload = await response.json();
337
+ } catch {
338
+ return null;
339
+ }
340
+
341
+ if (!response.ok || !payload?.success) {
342
+ return null;
343
+ }
344
+
345
+ return payload.result || {};
346
+ }
347
+
271
348
  async function resolveZoneIdForHost(host, token) {
272
349
  const candidates = guessZoneCandidates(host);
273
350
  for (const candidate of candidates) {
@@ -313,6 +390,52 @@ async function findRouteOwnerForHost(host) {
313
390
  return wildcard ? (wildcard.script || wildcard.script_name) : null;
314
391
  }
315
392
 
393
+ async function forceRouteBindingToWorker(host, workerName) {
394
+ const normalizedHost = normalizeHost(host);
395
+ const normalizedWorker = typeof workerName === 'string' ? workerName.trim() : '';
396
+ const token = getApiToken();
397
+ if (!normalizedHost || !normalizedWorker || !token) {
398
+ return false;
399
+ }
400
+
401
+ const zoneId = await resolveZoneIdForHost(normalizedHost, token);
402
+ if (!zoneId) {
403
+ return false;
404
+ }
405
+
406
+ const routes = await apiGetJson(`https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/routes`, token);
407
+ if (!Array.isArray(routes)) {
408
+ return false;
409
+ }
410
+
411
+ const exactPattern = `${normalizedHost}/*`;
412
+ const matchingRoute = routes.find((route) => String(route?.pattern || '') === exactPattern)
413
+ || routes.find((route) => routePatternMatchesHost(route?.pattern, normalizedHost));
414
+
415
+ if (matchingRoute?.id) {
416
+ const currentScript = matchingRoute.script || matchingRoute.script_name || '';
417
+ if (currentScript === normalizedWorker) {
418
+ return false;
419
+ }
420
+
421
+ const updated = await apiMutateJson(
422
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/routes/${matchingRoute.id}`,
423
+ token,
424
+ 'PUT',
425
+ { pattern: matchingRoute.pattern || exactPattern, script: normalizedWorker }
426
+ );
427
+ return Boolean(updated);
428
+ }
429
+
430
+ const created = await apiMutateJson(
431
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/routes`,
432
+ token,
433
+ 'POST',
434
+ { pattern: exactPattern, script: normalizedWorker }
435
+ );
436
+ return Boolean(created);
437
+ }
438
+
316
439
  async function maybeAlignWorkerNameToRouteOwner(configPath) {
317
440
  const text = fs.readFileSync(configPath, 'utf8');
318
441
  const host = extractRouteHost(text) || extractRelayUrlHost(text) || extractDatabaseDomainHost(text);
@@ -552,6 +675,19 @@ async function main() {
552
675
  }
553
676
  }
554
677
 
678
+ const finalText = fs.readFileSync(configPath, 'utf8');
679
+ const finalWorkerName = extractCurrentWorkerName(finalText);
680
+ if (finalWorkerName) {
681
+ const rebound = await forceRouteBindingToWorker(deployHost, finalWorkerName);
682
+ if (rebound) {
683
+ console.log(`Live health still stale on ${deployHost}. Rebound Cloudflare route to worker "${finalWorkerName}" and rechecking health.`);
684
+ liveVersion = await fetchLiveHealthVersion(deployHost);
685
+ if (liveVersion && compareVersions(liveVersion, CLI_VERSION) >= 0) {
686
+ process.exit(0);
687
+ }
688
+ }
689
+ }
690
+
555
691
  console.error(`Deploy completed but live health on https://${deployHost}/health is still stale (version=${liveVersion || 'unknown'}, expected>=${CLI_VERSION}).`);
556
692
  console.error('Refusing silent success. Please rerun deploy after Cloudflare route cache settles.');
557
693
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freertc",
3
- "version": "0.1.23",
3
+ "version": "0.1.28",
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.23';
10
+ const WORKER_VERSION = '0.1.28';
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.23";
2
+ const WORKER_VERSION = "0.1.28";
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"]);