@@ -29,18 +29,29 @@ const namespaceViewers = new Map();
2929const activeLoggerCounts = new Map ( ) ;
3030
3131// 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).
32+ // Matches namespace slugs that are either UUIDs or url-based slugs (e.g. https- www.domain1.com-3000 ).
3333const NAMESPACE_PATH_RE = / ^ \/ ( [ a - z A - Z 0 - 9 ] (?: [ a - z A - Z 0 - 9 . _ - ] * [ a - z A - Z 0 - 9 ] ) ? ) \/ ? $ / ;
3434
35- // Valid hostname characters (no colons so IPv6 addresses fall back to UUID).
35+ // Valid namespace characters (no colons so IPv6 addresses fall back to UUID).
3636const VALID_NAMESPACE_RE = / ^ [ a - z A - Z 0 - 9 ] (?: [ a - z A - Z 0 - 9 . _ - ] * [ a - z A - Z 0 - 9 ] ) ? $ / ;
3737
38- /** Derive a namespace slug from the WebSocket request's Origin header. */
38+ /** Derive a namespace slug from the WebSocket request's Origin header.
39+ * Uses the full URL (scheme + hostname + port) so that different origins
40+ * on the same host (e.g. http://localhost:3000 vs http://localhost:4000)
41+ * receive distinct namespaces.
42+ */
3943function getNamespaceFromOrigin ( originHeader ) {
4044 if ( ! originHeader ) return null ;
4145 try {
42- const { hostname } = new URL ( originHeader ) ;
43- return hostname && VALID_NAMESPACE_RE . test ( hostname ) ? hostname : null ;
46+ const { protocol, hostname, port } = new URL ( originHeader ) ;
47+ const scheme = protocol . slice ( 0 , - 1 ) ; // strip trailing colon, e.g. "http:" → "http"
48+ // The URL API normalises default ports (80/443) to an empty string, so two
49+ // origins that differ only by the explicit presence of the default port
50+ // (e.g. http://host and http://host:80) produce the same namespace.
51+ const namespace = port
52+ ? `${ scheme } -${ hostname } -${ port } `
53+ : `${ scheme } -${ hostname } ` ;
54+ return VALID_NAMESPACE_RE . test ( namespace ) ? namespace : null ;
4455 } catch {
4556 return null ;
4657 }
@@ -158,7 +169,9 @@ wss.on('connection', (ws, req) => {
158169 } else {
159170 // ── Logger connection ─────────────────────────────────────────────────
160171 // 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
172+ // the same origin share one namespace. The full URL (scheme + hostname +
173+ // port) is encoded into a path-safe slug so that different ports on the
174+ // same host get distinct namespaces. Fall back to a UUID when the
162175 // header is absent (e.g. Node.js clients) or contains an unusable value.
163176 const originNamespace = getNamespaceFromOrigin ( req . headers . origin ) ;
164177 const namespace = originNamespace ?? crypto . randomUUID ( ) ;
0 commit comments