openvibers/openvibe.shared url-resolver.js

url-resolver.js

main 21.2 KB · 598 lines · javascript

1'use strict';
2
3const URL_DEFINITIONS = Object.freeze({
4 OV_LIVE_URL: {
5 key: 'OV_LIVE_URL',
6 label: 'OpenVibe.Live Public URL',
7 category: 'public_first_party_urls',
8 service: 'live',
9 scope: 'global',
10 type: 'origin',
11 default: 'https://openvibe.live',
12 description: 'Canonical public origin for OpenVibe.Live.',
13 },
14 OV_TOOLS_URL: {
15 key: 'OV_TOOLS_URL',
16 label: 'OpenVibe.Tools Public URL',
17 category: 'public_first_party_urls',
18 service: 'tools',
19 scope: 'global',
20 type: 'origin',
21 default: 'https://openvibe.tools',
22 description: 'Canonical public origin for the OpenVibe.Tools gateway.',
23 },
24 OV_GAMES_URL: {
25 key: 'OV_GAMES_URL',
26 label: 'OpenVibe.Games Public URL',
27 category: 'public_first_party_urls',
28 service: 'games',
29 scope: 'global',
30 type: 'origin',
31 default: 'https://openvibe.games',
32 description: 'Canonical public origin for OpenVibe.Games.',
33 },
34 OV_MEDIA_URL: {
35 key: 'OV_MEDIA_URL',
36 label: 'OpenVibe.Media Public URL',
37 category: 'public_first_party_urls',
38 service: 'media',
39 scope: 'global',
40 type: 'origin',
41 default: 'https://openvibe.media',
42 description: 'Canonical public origin for OpenVibe.Media.',
43 },
44 WEBRTC_PUBLIC_URL: {
45 key: 'WEBRTC_PUBLIC_URL',
46 label: 'WebRTC Public URL',
47 category: 'protocol_ingest_urls',
48 service: 'live',
49 scope: 'global',
50 type: 'origin',
51 default: 'http://localhost:3000',
52 description: 'Public origin used for WebRTC endpoint discovery and browser consumers.',
53 },
54 WHIP_PUBLIC_URL: {
55 key: 'WHIP_PUBLIC_URL',
56 label: 'WHIP Public URL',
57 category: 'protocol_ingest_urls',
58 service: 'live',
59 scope: 'global',
60 type: 'origin',
61 default: 'http://localhost:3000',
62 description: 'Canonical WHIP ingestion origin for OBS and other WHIP clients.',
63 },
64 WHIP_PUBLIC_URL_ENABLED: {
65 key: 'WHIP_PUBLIC_URL_ENABLED',
66 label: 'Dedicated WHIP Host Enabled',
67 category: 'protocol_ingest_urls',
68 service: 'live',
69 scope: 'global',
70 type: 'boolean',
71 default: false,
72 description: 'Enable use of the dedicated WHIP hostname configured by WHIP_PUBLIC_URL. If disabled, the app falls back to the safe public WebRTC origin.',
73 },
74 JSMPEG_PUBLIC_URL: {
75 key: 'JSMPEG_PUBLIC_URL',
76 label: 'JSMPEG Public URL',
77 category: 'protocol_ingest_urls',
78 service: 'live',
79 scope: 'global',
80 type: 'origin',
81 default: 'http://localhost:3000',
82 description: 'Public origin used to construct JSMPEG relay endpoints for browsers and FFmpeg clients.',
83 },
84 TURN_URL: {
85 key: 'TURN_URL',
86 label: 'TURN URL',
87 category: 'protocol_ingest_urls',
88 service: 'live',
89 scope: 'global',
90 type: 'turn_url',
91 default: '',
92 description: 'TURN server connection string used for WebRTC NAT traversal.',
93 },
94 MEDIASOUP_ANNOUNCED_IP: {
95 key: 'MEDIASOUP_ANNOUNCED_IP',
96 label: 'Mediasoup Announced IP/Hostname',
97 category: 'protocol_ingest_urls',
98 service: 'live',
99 scope: 'global',
100 type: 'hostname',
101 default: 'localhost',
102 description: 'Hostname or public IP advertised in Mediasoup ICE candidates.',
103 },
104 RTMP_HOST: {
105 key: 'RTMP_HOST',
106 label: 'RTMP Host',
107 category: 'protocol_ingest_urls',
108 service: 'live',
109 scope: 'global',
110 type: 'rtmp_host',
111 default: '',
112 description: 'Hostname used for RTMP ingest. Do not include protocol or path.',
113 },
114 OV_NETWORK_URL: {
115 key: 'OV_NETWORK_URL',
116 label: 'OpenVibe.Network URL',
117 category: 'public_first_party_urls',
118 service: 'network',
119 scope: 'global',
120 type: 'origin',
121 default: 'https://openvibe.network',
122 description: 'Public URL for the OpenVibe.Network SSO, account, and registry service.',
123 },
124 OV_NETWORK_LOGIN_URL: {
125 key: 'OV_NETWORK_LOGIN_URL',
126 label: 'OpenVibe.Network Login URL',
127 category: 'public_first_party_urls',
128 service: 'network',
129 scope: 'global',
130 type: 'origin',
131 default: 'https://openvibe.network',
132 description: 'Public login URL for OpenVibe.Network. Typically the same as the public OpenVibe.Network origin.',
133 },
134 OV_NETWORK_INTERNAL_URL: {
135 key: 'OV_NETWORK_INTERNAL_URL',
136 label: 'OpenVibe.Network Internal URL',
137 category: 'internal_service_urls',
138 service: 'network',
139 scope: 'global',
140 type: 'internal_base_url',
141 default: 'http://127.0.0.1:4000',
142 description: 'Internal URL used by services to call the OpenVibe.Network internal API.',
143 },
144 OV_LIVE_INTERNAL_URL: {
145 key: 'OV_LIVE_INTERNAL_URL',
146 label: 'OpenVibe.Live Internal URL',
147 category: 'internal_service_urls',
148 service: 'live',
149 scope: 'global',
150 type: 'internal_base_url',
151 default: 'http://127.0.0.1:3000',
152 description: 'Internal URL used by OpenVibe.Network or other OpenVibe services to reach OpenVibe.Live.',
153 },
154 OV_TOOLS_INTERNAL_URL: {
155 key: 'OV_TOOLS_INTERNAL_URL',
156 label: 'OpenVibe.Tools Internal URL',
157 category: 'internal_service_urls',
158 service: 'tools',
159 scope: 'global',
160 type: 'internal_base_url',
161 default: 'http://127.0.0.1:4001',
162 description: 'Internal URL used by OpenVibe.Network to reach the OpenVibe.Tools gateway.',
163 },
164 OV_GAMES_INTERNAL_URL: {
165 key: 'OV_GAMES_INTERNAL_URL',
166 label: 'OpenVibe.Games Internal URL',
167 category: 'internal_service_urls',
168 service: 'games',
169 scope: 'global',
170 type: 'internal_base_url',
171 default: 'http://127.0.0.1:8000',
172 description: 'Internal URL used by OpenVibe.Network to reach OpenVibe.Games.',
173 },
174 OV_MEDIA_INTERNAL_URL: {
175 key: 'OV_MEDIA_INTERNAL_URL',
176 label: 'OpenVibe.Media Internal URL',
177 category: 'internal_service_urls',
178 service: 'media',
179 scope: 'global',
180 type: 'internal_base_url',
181 default: 'http://127.0.0.1:4100',
182 description: 'Internal URL used by OpenVibe.Network to reach OpenVibe.Media.',
183 },
184 OV_MAPS_INTERNAL_URL: {
185 key: 'OV_MAPS_INTERNAL_URL',
186 label: 'Maps.OpenVibe Internal URL',
187 category: 'internal_service_urls',
188 service: 'maps',
189 scope: 'global',
190 type: 'internal_base_url',
191 default: 'http://127.0.0.1:4010',
192 description: 'Internal URL of the Maps.OpenVibe tools satellite.',
193 },
194 OV_FOOD_INTERNAL_URL: {
195 key: 'OV_FOOD_INTERNAL_URL',
196 label: 'Food.OpenVibe Internal URL',
197 category: 'internal_service_urls',
198 service: 'food',
199 scope: 'global',
200 type: 'internal_base_url',
201 default: 'http://127.0.0.1:4011',
202 description: 'Internal URL of the Food.OpenVibe tools satellite.',
203 },
204 OV_IMG_INTERNAL_URL: {
205 key: 'OV_IMG_INTERNAL_URL',
206 label: 'Img.OpenVibe Internal URL',
207 category: 'internal_service_urls',
208 service: 'img',
209 scope: 'global',
210 type: 'internal_base_url',
211 default: 'http://127.0.0.1:4012',
212 description: 'Internal URL of the Img.OpenVibe tools satellite.',
213 },
214 OV_YT_INTERNAL_URL: {
215 key: 'OV_YT_INTERNAL_URL',
216 label: 'YT.OpenVibe Internal URL',
217 category: 'internal_service_urls',
218 service: 'yt',
219 scope: 'global',
220 type: 'internal_base_url',
221 default: 'http://127.0.0.1:4013',
222 description: 'Internal URL of the YT.OpenVibe tools satellite.',
223 },
224 OV_AUDIO_INTERNAL_URL: {
225 key: 'OV_AUDIO_INTERNAL_URL',
226 label: 'Audio.OpenVibe Internal URL',
227 category: 'internal_service_urls',
228 service: 'audio',
229 scope: 'global',
230 type: 'internal_base_url',
231 default: 'http://127.0.0.1:4014',
232 description: 'Internal URL of the Audio.OpenVibe tools satellite.',
233 },
234 OV_TEXT_INTERNAL_URL: {
235 key: 'OV_TEXT_INTERNAL_URL',
236 label: 'Text.OpenVibe Internal URL',
237 category: 'internal_service_urls',
238 service: 'text',
239 scope: 'global',
240 type: 'internal_base_url',
241 default: 'http://127.0.0.1:4015',
242 description: 'Internal URL of the Text.OpenVibe tools satellite.',
243 },
244 OV_DOCS_INTERNAL_URL: {
245 key: 'OV_DOCS_INTERNAL_URL',
246 label: 'Docs.OpenVibe Internal URL',
247 category: 'internal_service_urls',
248 service: 'docs',
249 scope: 'global',
250 type: 'internal_base_url',
251 default: 'http://127.0.0.1:4016',
252 description: 'Internal URL of the Docs.OpenVibe tools satellite.',
253 },
254
255 // ── Brand / Site Identity ─────────────────────────────────────────────────
256 // Configurable during first-time setup and editable in admin.
257 // These values drive display names, JWT metadata, email copy, and CORS policy.
258 // Defaults match the OpenVibe — change for white-label installs.
259
260 NETWORK_NAME: {
261 key: 'NETWORK_NAME',
262 label: 'Network / Organization Name',
263 category: 'brand_identity',
264 service: 'network',
265 scope: 'global',
266 type: 'string',
267 default: 'OpenVibe',
268 description: 'Human-readable name for the overall platform (e.g. "OpenVibe" or your custom brand).',
269 },
270 TOOLS_SERVICE_NAME: {
271 key: 'TOOLS_SERVICE_NAME',
272 label: 'Tools Service Name',
273 category: 'brand_identity',
274 service: 'network',
275 scope: 'global',
276 type: 'string',
277 default: 'OpenVibe.Tools',
278 description: 'Display name for the SSO/tools service (e.g. "OpenVibe.Tools" or "CoolTools").',
279 },
280 STREAMER_SERVICE_NAME: {
281 key: 'STREAMER_SERVICE_NAME',
282 label: 'Streamer Service Name',
283 category: 'brand_identity',
284 service: 'live',
285 scope: 'global',
286 type: 'string',
287 default: 'OpenVibe.Live',
288 description: 'Display name for the streaming service (e.g. "OpenVibe.Live").',
289 },
290 GAMES_SERVICE_NAME: {
291 key: 'GAMES_SERVICE_NAME',
292 label: 'Games Service Name',
293 category: 'brand_identity',
294 service: 'games',
295 scope: 'global',
296 type: 'string',
297 default: 'OpenVibe.Games',
298 description: 'Display name for the games service (e.g. "OpenVibe.Games").',
299 },
300 MEDIA_SERVICE_NAME: {
301 key: 'MEDIA_SERVICE_NAME',
302 label: 'Media Service Name',
303 category: 'brand_identity',
304 service: 'media',
305 scope: 'global',
306 type: 'string',
307 default: 'OpenVibe.Media',
308 description: 'Display name for the media service (e.g. "OpenVibe.Media").',
309 },
310
311 // ── CORS / Origin Policy ──────────────────────────────────────────────────
312 // Controls which additional origins are allowed cross-origin access.
313 // The public service URLs above are always auto-allowed.
314
315 TOOLS_SUBDOMAIN_BASE: {
316 key: 'TOOLS_SUBDOMAIN_BASE',
317 label: 'Tools Subdomain Base Domain',
318 category: 'cors_policy',
319 service: 'network',
320 scope: 'global',
321 type: 'string',
322 default: 'openvibe.tools',
323 description: 'Base domain for which all https://*.{domain} subdomains are trusted first-party origins (e.g. "openvibe.tools"). Leave empty to disable subdomain wildcarding.',
324 },
325 ALLOWED_EXTRA_ORIGINS: {
326 key: 'ALLOWED_EXTRA_ORIGINS',
327 label: 'Extra Allowed Origins (JSON array)',
328 category: 'cors_policy',
329 service: 'network',
330 scope: 'global',
331 type: 'json_array',
332 default: '[]',
333 description: 'JSON array of additional origins allowed by CORS across all services (e.g. ["https://my-cdn.com"]). Auto-includes all configured service public URLs.',
334 },
335
336 // ── Deploy Infrastructure ─────────────────────────────────────────────────
337 // Certificate, DNS, and reverse-proxy management config.
338 // These drive the automated TLS issuance and Nginx generation system.
339
340 DEPLOY_ACME_EMAIL: {
341 key: 'DEPLOY_ACME_EMAIL',
342 label: 'ACME / Let\'s Encrypt Email',
343 category: 'deploy_tls',
344 service: 'network',
345 scope: 'global',
346 type: 'string',
347 default: '',
348 description: 'Email address used for Let\'s Encrypt certificate registration and expiry notifications.',
349 },
350 DEPLOY_CERT_MODE: {
351 key: 'DEPLOY_CERT_MODE',
352 label: 'Certificate Mode',
353 category: 'deploy_tls',
354 service: 'network',
355 scope: 'global',
356 type: 'string',
357 default: 'manual',
358 description: 'Certificate provisioning mode: "cloudflare" for automated DNS-01 via Cloudflare API, "manual" for manual DNS challenge, or "none" to skip.',
359 },
360 DEPLOY_CLOUDFLARE_TOKEN: {
361 key: 'DEPLOY_CLOUDFLARE_TOKEN',
362 label: 'Cloudflare API Token (DNS Edit)',
363 category: 'deploy_tls',
364 service: 'network',
365 scope: 'global',
366 type: 'secret',
367 default: '',
368 description: 'Cloudflare API token with Zone:DNS:Edit permissions. Used for automated DNS-01 wildcard certificate challenges.',
369 },
370 DEPLOY_DOMAINS: {
371 key: 'DEPLOY_DOMAINS',
372 label: 'Managed Domains (JSON)',
373 category: 'deploy_tls',
374 service: 'network',
375 scope: 'global',
376 type: 'json_array',
377 default: '[]',
378 description: 'JSON array of domain objects managed by the deploy system. Each entry: {"domain":"openvibe.network","wildcard":false,"certName":"openvibe.network","services":["network"]}.',
379 },
380 DEPLOY_NGINX_MODE: {
381 key: 'DEPLOY_NGINX_MODE',
382 label: 'Nginx Management Mode',
383 category: 'deploy_nginx',
384 service: 'network',
385 scope: 'global',
386 type: 'string',
387 default: 'preview',
388 description: 'Nginx config management mode: "preview" (generate only), "apply" (write + validate + reload), or "disabled".',
389 },
390 DEPLOY_NGINX_SITES_PATH: {
391 key: 'DEPLOY_NGINX_SITES_PATH',
392 label: 'Nginx sites-enabled Path',
393 category: 'deploy_nginx',
394 service: 'network',
395 scope: 'global',
396 type: 'string',
397 default: '/etc/nginx/sites-enabled',
398 description: 'Directory where Nginx site configs are written. Defaults to /etc/nginx/sites-enabled.',
399 },
400 DEPLOY_NGINX_BACKUP_PATH: {
401 key: 'DEPLOY_NGINX_BACKUP_PATH',
402 label: 'Nginx Backup Path',
403 category: 'deploy_nginx',
404 service: 'network',
405 scope: 'global',
406 type: 'string',
407 default: '/etc/nginx/sites-backup',
408 description: 'Directory for Nginx config backups before apply. Created automatically.',
409 },
410 DEPLOY_SERVICE_MAP: {
411 key: 'DEPLOY_SERVICE_MAP',
412 label: 'Service → Port / Domain Map (JSON)',
413 category: 'deploy_nginx',
414 service: 'network',
415 scope: 'global',
416 type: 'json_map',
417 default: '{}',
418 description: 'JSON map of service configurations for Nginx generation. Each key is a service ID, value has port, domains[], wildcardDomain, maxBodySize, etc.',
419 },
420});
421
422function normalizeOrigin(value) {
423 if (!value || typeof value !== 'string') return null;
424 try {
425 const url = new URL(value.trim());
426 if (!url.protocol || !url.hostname) return null;
427 if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
428 return `${url.protocol}//${url.hostname}${url.port ? `:${url.port}` : ''}`;
429 } catch {
430 return null;
431 }
432}
433
434function normalizeInternalBaseUrl(value) {
435 if (!value || typeof value !== 'string') return null;
436 try {
437 const url = new URL(value.trim());
438 if (!url.protocol || !url.hostname) return null;
439 if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
440 return `${url.protocol}//${url.hostname}${url.port ? `:${url.port}` : ''}`;
441 } catch {
442 return null;
443 }
444}
445
446function normalizeWsOrigin(value) {
447 if (!value || typeof value !== 'string') return null;
448 try {
449 const url = new URL(value.trim());
450 if (!url.hostname) return null;
451 if (url.protocol !== 'ws:' && url.protocol !== 'wss:') return null;
452 return `${url.protocol}//${url.hostname}${url.port ? `:${url.port}` : ''}`;
453 } catch {
454 return null;
455 }
456}
457
458function normalizeHostname(value) {
459 if (!value || typeof value !== 'string') return null;
460 const trimmed = value.trim();
461 if (/^\w+:\/\//.test(trimmed)) {
462 try {
463 const url = new URL(trimmed);
464 return url.hostname || null;
465 } catch {
466 return null;
467 }
468 }
469 if (/[^a-zA-Z0-9.\-:]/.test(trimmed)) return null;
470 return trimmed;
471}
472
473function normalizeTurnUrl(value) {
474 if (!value || typeof value !== 'string') return null;
475 let trimmed = value.trim();
476 if (/^turns?:[^/]/i.test(trimmed)) {
477 trimmed = trimmed.replace(/^turns?:/i, (s) => s + '//');
478 }
479 if (!/^turns?:\/\//i.test(trimmed)) return null;
480 try {
481 const url = new URL(trimmed);
482 if (!url.hostname) return null;
483 const protocol = url.protocol.toLowerCase();
484 if (protocol !== 'turn:' && protocol !== 'turns:') return null;
485 if (url.username || url.password) return null;
486 return `${protocol}${url.hostname}${url.port ? `:${url.port}` : ''}${url.pathname}${url.search}`;
487 } catch {
488 return null;
489 }
490}
491
492function normalizeBoolean(value) {
493 if (value === true || value === 1 || value === '1' || value === 'true' || value === 'TRUE' || value === 'True') {
494 return true;
495 }
496 if (value === false || value === 0 || value === '0' || value === 'false' || value === 'FALSE' || value === 'False') {
497 return false;
498 }
499 return null;
500}
501
502function normalizeJson(value, type) {
503 if (typeof value === 'string') {
504 try {
505 return JSON.parse(value);
506 } catch {
507 return null;
508 }
509 }
510 if (type === 'json_map' && typeof value === 'object' && value !== null && !Array.isArray(value)) {
511 return value;
512 }
513 if (type === 'json_array' && Array.isArray(value)) {
514 return value;
515 }
516 return null;
517}
518
519function normalizeValue(value, type) {
520 if (value === undefined || value === null) return null;
521 switch (type) {
522 case 'origin': return normalizeOrigin(value);
523 case 'internal_base_url': return normalizeInternalBaseUrl(value);
524 case 'ws_origin': return normalizeWsOrigin(value);
525 case 'hostname': return normalizeHostname(value);
526 case 'rtmp_host': return normalizeHostname(value);
527 case 'turn_url': return normalizeTurnUrl(value);
528 case 'json_map': return normalizeJson(value, 'json_map');
529 case 'json_array': return normalizeJson(value, 'json_array');
530 case 'boolean': return normalizeBoolean(value);
531 case 'secret': return typeof value === 'string' ? value.trim() : String(value);
532 case 'string': return typeof value === 'string' ? value.trim() : String(value);
533 default: return typeof value === 'string' ? value.trim() : String(value);
534 }
535}
536
537function validateValue(value, type) {
538 return normalizeValue(value, type) !== null;
539}
540
541function resolveRegistryValues(env = {}, overrides = {}, bootstrap = {}, definitions = URL_DEFINITIONS) {
542 const resolved = {};
543 for (const [key, def] of Object.entries(definitions)) {
544 let rawValue = def.default;
545 let source = 'default';
546 if (env[key]) {
547 rawValue = env[key];
548 source = 'env';
549 }
550 if (bootstrap[key] != null && bootstrap[key] !== '') {
551 rawValue = bootstrap[key];
552 source = 'bootstrap';
553 }
554 if (overrides[key] != null && overrides[key] !== '') {
555 rawValue = overrides[key];
556 source = 'admin';
557 }
558 const normalized = normalizeValue(rawValue, def.type);
559 resolved[key] = {
560 key,
561 label: def.label,
562 category: def.category,
563 service: def.service,
564 scope: def.scope,
565 type: def.type,
566 description: def.description,
567 value: normalized,
568 source,
569 };
570 }
571 return resolved;
572}
573
574function formatRegistryEntry(entry) {
575 if (!entry || typeof entry !== 'object') return null;
576 return {
577 key: entry.key,
578 label: entry.label,
579 category: entry.category,
580 service: entry.service,
581 scope: entry.scope,
582 type: entry.type,
583 description: entry.description,
584 value: entry.value || null,
585 source: entry.source || 'admin',
586 updatedBy: entry.updated_by || null,
587 updatedAt: entry.updated_at || null,
588 };
589}
590
591module.exports = {
592 URL_DEFINITIONS,
593 normalizeValue,
594 validateValue,
595 resolveRegistryValues,
596 formatRegistryEntry,
597};
598