From c309631bc462ad75240f07bf2646a9cd15cea85e Mon Sep 17 00:00:00 2001 From: Priyanshubhartistm Date: Mon, 20 Jul 2026 15:25:53 +0530 Subject: [PATCH 1/2] fix(web-app): map ws:// relay URL to http:// for CSP connect-src Signed-off-by: Priyanshubhartistm --- .changeset/fix-csp-ws-http-scheme-mapping.md | 10 ++++++++++ src/factories/web-app-factory.ts | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-csp-ws-http-scheme-mapping.md diff --git a/.changeset/fix-csp-ws-http-scheme-mapping.md b/.changeset/fix-csp-ws-http-scheme-mapping.md new file mode 100644 index 00000000..fe29e22f --- /dev/null +++ b/.changeset/fix-csp-ws-http-scheme-mapping.md @@ -0,0 +1,10 @@ +--- +"nostream": patch +--- + +Fix the Content-Security-Policy `connect-src` directive for relays served over plain `ws://`. + +The web app factory derived an HTTP(S) origin from the relay's WebSocket URL but mapped +`ws:` to the invalid scheme `':'`, which the WHATWG URL API silently ignores. As a result the +`connect-src` directive kept a `ws://…` entry instead of the intended `http://…` origin for +local/dev, Tor, or reverse-proxied setups. The `ws:` protocol now correctly maps to `http:`. diff --git a/src/factories/web-app-factory.ts b/src/factories/web-app-factory.ts index 697b262c..408d340b 100644 --- a/src/factories/web-app-factory.ts +++ b/src/factories/web-app-factory.ts @@ -15,7 +15,7 @@ export const createWebApp = (): Express => { const relayUrl = new URL(settings.info.relay_url) const webRelayUrl = new URL(relayUrl.toString()) - webRelayUrl.protocol = relayUrl.protocol === 'wss:' ? 'https:' : ':' + webRelayUrl.protocol = relayUrl.protocol === 'wss:' ? 'https:' : 'http:' const directives = { 'img-src': ["'self'", 'data:', 'https://cdn.zebedee.io/an/nostr/'], From b824c4524f7d7b0e94764a3e53ca4c79e512026f Mon Sep 17 00:00:00 2001 From: Priyanshubhartistm Date: Sat, 15 Aug 2026 21:18:54 +0530 Subject: [PATCH 2/2] test(web-app): add regression coverage for ws/wss CSP protocol mapping Signed-off-by: Priyanshubhartistm --- .changeset/fix-csp-ws-http-scheme-mapping.md | 4 ++++ src/factories/web-app-factory.ts | 6 +++++- test/unit/factories/web-app-factory.spec.ts | 13 +++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 test/unit/factories/web-app-factory.spec.ts diff --git a/.changeset/fix-csp-ws-http-scheme-mapping.md b/.changeset/fix-csp-ws-http-scheme-mapping.md index fe29e22f..6a41f3bc 100644 --- a/.changeset/fix-csp-ws-http-scheme-mapping.md +++ b/.changeset/fix-csp-ws-http-scheme-mapping.md @@ -8,3 +8,7 @@ The web app factory derived an HTTP(S) origin from the relay's WebSocket URL but `ws:` to the invalid scheme `':'`, which the WHATWG URL API silently ignores. As a result the `connect-src` directive kept a `ws://…` entry instead of the intended `http://…` origin for local/dev, Tor, or reverse-proxied setups. The `ws:` protocol now correctly maps to `http:`. + +Adds regression test coverage for the protocol mapping (`getWebProtocolForRelay`, extracted from +`createWebApp` so it can be unit tested directly), since this file previously had no test coverage +at all. diff --git a/src/factories/web-app-factory.ts b/src/factories/web-app-factory.ts index 88112ab5..39b25621 100644 --- a/src/factories/web-app-factory.ts +++ b/src/factories/web-app-factory.ts @@ -5,6 +5,10 @@ import { createSettings } from './settings-factory' import router from '../routes' import { getGrafanaFrameOrigin } from '../utils/admin-grafana' +// Extracted so the ws:/wss: -> http:/https: mapping can be unit tested directly, +// without needing to exercise the full Express middleware stack. +export const getWebProtocolForRelay = (relayProtocol: string): string => (relayProtocol === 'wss:' ? 'https:' : 'http:') + export const createWebApp = (): Express => { const app = express() app @@ -16,7 +20,7 @@ export const createWebApp = (): Express => { const relayUrl = new URL(settings.info.relay_url) const webRelayUrl = new URL(relayUrl.toString()) - webRelayUrl.protocol = relayUrl.protocol === 'wss:' ? 'https:' : 'http:' + webRelayUrl.protocol = getWebProtocolForRelay(relayUrl.protocol) const directives = { 'img-src': ["'self'", 'data:', 'https://cdn.zebedee.io/an/nostr/'], diff --git a/test/unit/factories/web-app-factory.spec.ts b/test/unit/factories/web-app-factory.spec.ts new file mode 100644 index 00000000..c3f7e274 --- /dev/null +++ b/test/unit/factories/web-app-factory.spec.ts @@ -0,0 +1,13 @@ +import { expect } from 'chai' + +import { getWebProtocolForRelay } from '../../../src/factories/web-app-factory' + +describe('getWebProtocolForRelay', () => { + it('maps wss: to https:', () => { + expect(getWebProtocolForRelay('wss:')).to.equal('https:') + }) + + it('maps ws: to http: (regression: previously mapped to the invalid scheme ":")', () => { + expect(getWebProtocolForRelay('ws:')).to.equal('http:') + }) +})