Bump CACHEBUST to v8 for backend and frontend rebuilds; update Nginx and NostrAuthGuard to handle X-Forwarded-Prefix for NIP-98 compliance

This commit is contained in:
Dorian
2026-02-13 20:20:32 +00:00
parent dea2d2e768
commit abb83fe164
5 changed files with 27 additions and 10 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;

View File

@@ -119,18 +119,22 @@ class AuthService {
* Signs a kind-27235 event and sends it as the Authorization header.
*/
async createNostrSession(_request: NostrSessionRequest): Promise<NostrSessionResponse> {
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<NostrSessionResponse>({
method: 'POST',
url,
url: absoluteUrl,
headers: { Authorization: authHeader },
timeout: apiConfig.timeout,
})

View File

@@ -67,15 +67,19 @@ class Nip98Service {
*/
async createSession(signer: any, pubkey: string): Promise<boolean> {
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}`,
},