diff --git a/backend/src/nostr-auth/nostr-auth.guard.ts b/backend/src/nostr-auth/nostr-auth.guard.ts index a11ceeb..d9d0c38 100644 --- a/backend/src/nostr-auth/nostr-auth.guard.ts +++ b/backend/src/nostr-auth/nostr-auth.guard.ts @@ -88,9 +88,15 @@ export class NostrAuthGuard extends AuthGuard('nostr') { 'http'; const host = request.get('host') ?? request.headers.host ?? request.hostname; + + // When behind a reverse proxy that strips a path prefix (e.g. /api), + // the proxy should forward X-Forwarded-Prefix so we can reconstruct + // the original URL that the client signed in its NIP-98 event. + const prefix = + (request.headers['x-forwarded-prefix'] as string | undefined) ?? ''; const path = request.originalUrl ?? request.url ?? ''; - return `${protocolHeader}://${host}${path}`; + return `${protocolHeader}://${host}${prefix}${path}`; } private mapErrorToHttpException(error: NostrAuthError) { diff --git a/docker-compose.yml b/docker-compose.yml index 1ee6ed7..940ff3e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: context: . dockerfile: Dockerfile args: - CACHEBUST: "7" + CACHEBUST: "8" VITE_USE_MOCK_DATA: "false" VITE_CONTENT_ORIGIN: ${FRONTEND_URL} VITE_INDEEHUB_API_URL: /api @@ -47,7 +47,7 @@ services: context: ./backend dockerfile: Dockerfile args: - CACHEBUST: "7" + CACHEBUST: "8" restart: unless-stopped environment: # ── Core ───────────────────────────────────────────── @@ -179,7 +179,7 @@ services: context: ./backend dockerfile: Dockerfile.ffmpeg args: - CACHEBUST: "7" + CACHEBUST: "8" restart: unless-stopped environment: ENVIRONMENT: production diff --git a/nginx.conf b/nginx.conf index 7ffee30..82747f1 100644 --- a/nginx.conf +++ b/nginx.conf @@ -41,6 +41,9 @@ server { # Trust the outer reverse proxy's X-Forwarded-Proto when present, # otherwise fall back to the connection scheme. proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + # Preserve the original /api prefix so NIP-98 URL verification + # can reconstruct the URL the client actually signed. + proxy_set_header X-Forwarded-Prefix /api; proxy_read_timeout 300s; proxy_send_timeout 300s; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index ce83a05..5bff671 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -119,18 +119,22 @@ class AuthService { * Signs a kind-27235 event and sends it as the Authorization header. */ async createNostrSession(_request: NostrSessionRequest): Promise { - const url = `${apiConfig.baseURL}/auth/nostr/session` + const relativeUrl = `${apiConfig.baseURL}/auth/nostr/session` const method = 'POST' + // NIP-98 requires an absolute URL in the event tag so it matches + // what the backend reconstructs from Host / X-Forwarded-Proto. + const absoluteUrl = new URL(relativeUrl, window.location.origin).toString() + // Create NIP-98 auth header — no body is sent - const authHeader = await createNip98AuthHeader(url, method) + const authHeader = await createNip98AuthHeader(absoluteUrl, method) // Send the request without a body. // We use axios({ method }) instead of axios.post(url, data) to // guarantee no Content-Type or body is serialized. const response = await axios({ method: 'POST', - url, + url: absoluteUrl, headers: { Authorization: authHeader }, timeout: apiConfig.timeout, }) diff --git a/src/services/nip98.service.ts b/src/services/nip98.service.ts index 671963d..e205cee 100644 --- a/src/services/nip98.service.ts +++ b/src/services/nip98.service.ts @@ -67,15 +67,19 @@ class Nip98Service { */ async createSession(signer: any, pubkey: string): Promise { try { - const url = `${indeehubApiConfig.baseURL}/auth/nostr/session` + const relativeUrl = `${indeehubApiConfig.baseURL}/auth/nostr/session` const now = Math.floor(Date.now() / 1000) + // NIP-98 requires an absolute URL in the event tag so it matches + // what the backend reconstructs from Host / X-Forwarded-Proto. + const absoluteUrl = new URL(relativeUrl, window.location.origin).toString() + // Build the NIP-98 event const event = { kind: 27235, created_at: now, tags: [ - ['u', url], + ['u', absoluteUrl], ['method', 'POST'], ], content: '', @@ -98,7 +102,7 @@ class Nip98Service { // Send to backend — no body to avoid NIP-98 payload mismatch const response = await axios({ method: 'POST', - url, + url: absoluteUrl, headers: { Authorization: `Nostr ${encodedEvent}`, },