freertc 0.1.23 → 0.1.31

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.31","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 extractRouteHost(text) || extractDatabaseDomainHost(text) || extractRelayUrlHost(text) || extractGlobalRelayUrlHost(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,22 +183,30 @@ function extractDatabaseDomainHost(configText) {
134
183
  function autoPatchWranglerConfig(configPath) {
135
184
  let text = fs.readFileSync(configPath, 'utf8');
136
185
 
186
+ const dbDomainHost = extractDatabaseDomainHost(text);
137
187
  const routeHost = extractRouteHost(text);
138
- const envDomainHost = normalizeHost(process.env.CF_DOMAIN || process.env.CLOUD_FLARE_DOMAIN || '');
139
- const relayHost = routeHost || envDomainHost || extractRelayUrlHost(text) || extractDatabaseDomainHost(text);
140
-
141
- if (!relayHost) {
188
+ const relayUrlHost = extractRelayUrlHost(text);
189
+ const globalRelayHost = extractGlobalRelayUrlHost(text);
190
+ const publicHost = dbDomainHost || routeHost || relayUrlHost || globalRelayHost;
191
+ const envDomainCandidate = normalizeHost(process.env.CF_DOMAIN || process.env.CLOUD_FLARE_DOMAIN || '');
192
+ const envDomainHost = envDomainCandidate && !isPlaceholderHost(envDomainCandidate) ? envDomainCandidate : null;
193
+ const baseRelayDomain = dbDomainHost || routeHost || envDomainHost || relayUrlHost || globalRelayHost;
194
+
195
+ if (!baseRelayDomain) {
142
196
  return;
143
197
  }
144
198
 
199
+ const relayHost = baseRelayDomain;
200
+
145
201
  const relayWsUrl = `wss://${relayHost}/ws`;
146
202
  const relayName = relayHost;
147
- const dbName = `freertc-signal-${sanitizeDomain(relayHost)}`;
203
+ const dbName = `freertc-signal-${sanitizeDomain(dbDomainHost || baseRelayDomain)}`;
148
204
 
149
205
  const before = text;
150
206
  text = patchJsoncVar(text, 'RELAY_URL', relayWsUrl);
151
207
  text = patchJsoncVar(text, 'RELAY_NAME', relayName);
152
208
  text = text.replace(/("database_name"\s*:\s*)"freertc-signal(?:-your[-_]?domain)?"/gi, `$1"${dbName}"`);
209
+ text = ensureRoutesForHost(text, publicHost || relayHost);
153
210
 
154
211
  if (text !== before) {
155
212
  fs.writeFileSync(configPath, text, 'utf8');
@@ -198,8 +255,11 @@ function validateWranglerConfigForDeploy(configPath) {
198
255
 
199
256
  function extractRouteConflictOwner(outputText) {
200
257
  if (!outputText) return null;
201
- const match = outputText.match(/used by Worker:\s*([A-Za-z0-9._-]+)/i);
202
- return match?.[1] || null;
258
+ const usedByMatch = outputText.match(/used by Worker:\s*([A-Za-z0-9._-]+)/i);
259
+ if (usedByMatch?.[1]) return usedByMatch[1];
260
+
261
+ const assignedMatch = outputText.match(/"([A-Za-z0-9._-]+)"\s+is already assigned to routes:/i);
262
+ return assignedMatch?.[1] || null;
203
263
  }
204
264
 
205
265
  function patchWranglerScriptName(configPath, workerName) {
@@ -268,6 +328,31 @@ async function apiGetJson(url, token) {
268
328
  return payload.result;
269
329
  }
270
330
 
331
+ async function apiMutateJson(url, token, method, body) {
332
+ const response = await fetch(url, {
333
+ method,
334
+ headers: {
335
+ Authorization: `Bearer ${token}`,
336
+ Accept: 'application/json',
337
+ 'Content-Type': 'application/json'
338
+ },
339
+ body: JSON.stringify(body)
340
+ });
341
+
342
+ let payload;
343
+ try {
344
+ payload = await response.json();
345
+ } catch {
346
+ return null;
347
+ }
348
+
349
+ if (!response.ok || !payload?.success) {
350
+ return null;
351
+ }
352
+
353
+ return payload.result || {};
354
+ }
355
+
271
356
  async function resolveZoneIdForHost(host, token) {
272
357
  const candidates = guessZoneCandidates(host);
273
358
  for (const candidate of candidates) {
@@ -313,6 +398,52 @@ async function findRouteOwnerForHost(host) {
313
398
  return wildcard ? (wildcard.script || wildcard.script_name) : null;
314
399
  }
315
400
 
401
+ async function forceRouteBindingToWorker(host, workerName) {
402
+ const normalizedHost = normalizeHost(host);
403
+ const normalizedWorker = typeof workerName === 'string' ? workerName.trim() : '';
404
+ const token = getApiToken();
405
+ if (!normalizedHost || !normalizedWorker || !token) {
406
+ return false;
407
+ }
408
+
409
+ const zoneId = await resolveZoneIdForHost(normalizedHost, token);
410
+ if (!zoneId) {
411
+ return false;
412
+ }
413
+
414
+ const routes = await apiGetJson(`https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/routes`, token);
415
+ if (!Array.isArray(routes)) {
416
+ return false;
417
+ }
418
+
419
+ const exactPattern = `${normalizedHost}/*`;
420
+ const matchingRoute = routes.find((route) => String(route?.pattern || '') === exactPattern)
421
+ || routes.find((route) => routePatternMatchesHost(route?.pattern, normalizedHost));
422
+
423
+ if (matchingRoute?.id) {
424
+ const currentScript = matchingRoute.script || matchingRoute.script_name || '';
425
+ if (currentScript === normalizedWorker) {
426
+ return false;
427
+ }
428
+
429
+ const updated = await apiMutateJson(
430
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/routes/${matchingRoute.id}`,
431
+ token,
432
+ 'PUT',
433
+ { pattern: matchingRoute.pattern || exactPattern, script: normalizedWorker }
434
+ );
435
+ return Boolean(updated);
436
+ }
437
+
438
+ const created = await apiMutateJson(
439
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/routes`,
440
+ token,
441
+ 'POST',
442
+ { pattern: exactPattern, script: normalizedWorker }
443
+ );
444
+ return Boolean(created);
445
+ }
446
+
316
447
  async function maybeAlignWorkerNameToRouteOwner(configPath) {
317
448
  const text = fs.readFileSync(configPath, 'utf8');
318
449
  const host = extractRouteHost(text) || extractRelayUrlHost(text) || extractDatabaseDomainHost(text);
@@ -552,6 +683,19 @@ async function main() {
552
683
  }
553
684
  }
554
685
 
686
+ const finalText = fs.readFileSync(configPath, 'utf8');
687
+ const finalWorkerName = extractCurrentWorkerName(finalText);
688
+ if (finalWorkerName) {
689
+ const rebound = await forceRouteBindingToWorker(deployHost, finalWorkerName);
690
+ if (rebound) {
691
+ console.log(`Live health still stale on ${deployHost}. Rebound Cloudflare route to worker "${finalWorkerName}" and rechecking health.`);
692
+ liveVersion = await fetchLiveHealthVersion(deployHost);
693
+ if (liveVersion && compareVersions(liveVersion, CLI_VERSION) >= 0) {
694
+ process.exit(0);
695
+ }
696
+ }
697
+ }
698
+
555
699
  console.error(`Deploy completed but live health on https://${deployHost}/health is still stale (version=${liveVersion || 'unknown'}, expected>=${CLI_VERSION}).`);
556
700
  console.error('Refusing silent success. Please rerun deploy after Cloudflare route cache settles.');
557
701
  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.31",
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.31';
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.31";
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"]);