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 +1 -1
- package/bin/freertc.mjs +156 -12
- package/package.json +1 -1
- package/scripts/non-cloudflare-server.mjs +1 -1
- package/src/index.js +1 -1
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.
|
|
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(
|
|
137
|
+
const routeMatch = configText.match(/^\s*"pattern"\s*:\s*"([^"]+)"/m);
|
|
100
138
|
if (!routeMatch) return null;
|
|
101
139
|
const pattern = routeMatch[1];
|
|
102
|
-
|
|
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) ||
|
|
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
|
-
|
|
120
|
-
|
|
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
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
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(
|
|
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
|
|
202
|
-
|
|
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
|
@@ -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.
|
|
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.
|
|
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"]);
|