@@ -23,13 +23,28 @@ const MIME_TYPES = {
2323} ;
2424
2525// Namespace management
26- // Map from namespace UUID → Set of viewer WebSocket connections
26+ // Map from namespace → Set of viewer WebSocket connections
2727const namespaceViewers = new Map ( ) ;
28- // Set of namespace UUIDs that currently have an active logger connection
29- const activeLoggers = new Set ( ) ;
30-
31- // UUID pattern used to identify viewer connections by URL path
32- const UUID_PATH_RE = / ^ \/ ( [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 12 } ) \/ ? $ / i;
28+ // Map from namespace → count of active logger connections
29+ const activeLoggerCounts = new Map ( ) ;
30+
31+ // Pattern used to identify viewer connections by URL path.
32+ // Matches namespace slugs that are either UUIDs or origin hostnames (e.g. www.domain1.com).
33+ const NAMESPACE_PATH_RE = / ^ \/ ( [ a - z A - Z 0 - 9 ] (?: [ a - z A - Z 0 - 9 . _ - ] * [ a - z A - Z 0 - 9 ] ) ? ) \/ ? $ / ;
34+
35+ // Valid hostname characters (no colons so IPv6 addresses fall back to UUID).
36+ const VALID_NAMESPACE_RE = / ^ [ a - z A - Z 0 - 9 ] (?: [ a - z A - Z 0 - 9 . _ - ] * [ a - z A - Z 0 - 9 ] ) ? $ / ;
37+
38+ /** Derive a namespace slug from the WebSocket request's Origin header. */
39+ function getNamespaceFromOrigin ( originHeader ) {
40+ if ( ! originHeader ) return null ;
41+ try {
42+ const { hostname } = new URL ( originHeader ) ;
43+ return hostname && VALID_NAMESPACE_RE . test ( hostname ) ? hostname : null ;
44+ } catch {
45+ return null ;
46+ }
47+ }
3348
3449// HTTP server — serves the built React client from dist/
3550const server = http . createServer ( ( req , res ) => {
@@ -63,7 +78,7 @@ const server = http.createServer((req, res) => {
6378 // Return the list of namespaces that currently have an active logger
6479 if ( reqUrl === '/api/namespaces' ) {
6580 res . writeHead ( 200 , { 'Content-Type' : 'application/json' } ) ;
66- res . end ( JSON . stringify ( [ ...activeLoggers ] ) ) ;
81+ res . end ( JSON . stringify ( [ ...activeLoggerCounts . keys ( ) ] ) ) ;
6782 return ;
6883 }
6984
@@ -108,11 +123,11 @@ server.listen(PORT, () => {
108123
109124wss . on ( 'connection' , ( ws , req ) => {
110125 const urlPath = ( req . url ?? '/' ) . split ( '?' ) [ 0 ] ;
111- const uuidMatch = urlPath . match ( UUID_PATH_RE ) ;
126+ const namespaceMatch = urlPath . match ( NAMESPACE_PATH_RE ) ;
112127
113- if ( uuidMatch ) {
128+ if ( namespaceMatch ) {
114129 // ── Viewer connection ─────────────────────────────────────────────────
115- const namespace = uuidMatch [ 1 ] . toLowerCase ( ) ;
130+ const namespace = namespaceMatch [ 1 ] . toLowerCase ( ) ;
116131 if ( ! namespaceViewers . has ( namespace ) ) {
117132 namespaceViewers . set ( namespace , new Set ( ) ) ;
118133 }
@@ -130,7 +145,7 @@ wss.on('connection', (ws, req) => {
130145 const viewers = namespaceViewers . get ( namespace ) ;
131146 if ( viewers ) {
132147 viewers . delete ( ws ) ;
133- if ( viewers . size === 0 && ! activeLoggers . has ( namespace ) ) {
148+ if ( viewers . size === 0 && ! activeLoggerCounts . has ( namespace ) ) {
134149 namespaceViewers . delete ( namespace ) ;
135150 }
136151 }
@@ -142,9 +157,16 @@ wss.on('connection', (ws, req) => {
142157 } ) ;
143158 } else {
144159 // ── Logger connection ─────────────────────────────────────────────────
145- const namespace = crypto . randomUUID ( ) ;
146- activeLoggers . add ( namespace ) ;
147- namespaceViewers . set ( namespace , new Set ( ) ) ;
160+ // Derive namespace from the Origin header so that all connections from
161+ // the same origin share one namespace. Fall back to a UUID when the
162+ // header is absent (e.g. Node.js clients) or contains an unusable value.
163+ const originNamespace = getNamespaceFromOrigin ( req . headers . origin ) ;
164+ const namespace = originNamespace ?? crypto . randomUUID ( ) ;
165+
166+ activeLoggerCounts . set ( namespace , ( activeLoggerCounts . get ( namespace ) ?? 0 ) + 1 ) ;
167+ if ( ! namespaceViewers . has ( namespace ) ) {
168+ namespaceViewers . set ( namespace , new Set ( ) ) ;
169+ }
148170 console . log ( `Logger connected | namespace: ${ namespace } ` ) ;
149171 console . log ( `View logs at: http://localhost:${ PORT } /${ namespace } ` ) ;
150172
@@ -175,10 +197,15 @@ wss.on('connection', (ws, req) => {
175197 } ) ;
176198
177199 ws . on ( 'close' , ( ) => {
178- activeLoggers . delete ( namespace ) ;
179- const viewers = namespaceViewers . get ( namespace ) ;
180- if ( viewers && viewers . size === 0 ) {
181- namespaceViewers . delete ( namespace ) ;
200+ const count = ( activeLoggerCounts . get ( namespace ) ?? 1 ) - 1 ;
201+ if ( count <= 0 ) {
202+ activeLoggerCounts . delete ( namespace ) ;
203+ const viewers = namespaceViewers . get ( namespace ) ;
204+ if ( viewers && viewers . size === 0 ) {
205+ namespaceViewers . delete ( namespace ) ;
206+ }
207+ } else {
208+ activeLoggerCounts . set ( namespace , count ) ;
182209 }
183210 console . log ( `Logger disconnected | namespace: ${ namespace } ` ) ;
184211 } ) ;
0 commit comments