From b76d39a75e2d7c7abd78750cafad03390daf751d Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Wed, 1 Jul 2026 09:44:31 -0400 Subject: [PATCH] fix: handle Listenarr CSRF antiforgery via cookie jar --- backend/package.json | 2 ++ backend/src/services/listenarr.ts | 42 +++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/backend/package.json b/backend/package.json index 26041d9..2c5f441 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,6 +19,7 @@ "@solana/spl-token": "^0.4.14", "@solana/web3.js": "^1.87.6", "axios": "^1.6.2", + "axios-cookiejar-support": "^5.0.5", "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^16.3.1", @@ -30,6 +31,7 @@ "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", "socket.io": "^4.7.3", + "tough-cookie": "^5.1.2", "tweetnacl": "^1.0.3", "ws": "^8.15.1" }, diff --git a/backend/src/services/listenarr.ts b/backend/src/services/listenarr.ts index 7824535..102fe7b 100644 --- a/backend/src/services/listenarr.ts +++ b/backend/src/services/listenarr.ts @@ -1,16 +1,36 @@ -import axios from "axios"; +import axios, { type AxiosInstance } from "axios"; +import { wrapper } from "axios-cookiejar-support"; +import { CookieJar } from "tough-cookie"; const LISTENARR_URL = process.env.LISTENARR_URL || ""; const LISTENARR_API_KEY = process.env.LISTENARR_API_KEY || ""; -const listenarrApi = axios.create({ - baseURL: LISTENARR_URL, - timeout: 15000, - headers: { - "X-Api-Key": LISTENARR_API_KEY, - "Content-Type": "application/json", - }, -}); +// ponytail: single shared cookie jar so CSRF cookies persist across requests +const jar = new CookieJar(); +const listenarrApi: AxiosInstance = wrapper( + axios.create({ + baseURL: LISTENARR_URL, + timeout: 15000, + headers: { + "Content-Type": "application/json", + ...(LISTENARR_API_KEY ? { "X-Api-Key": LISTENARR_API_KEY } : {}), + }, + withCredentials: true, + jar, + }), +); + +// ponytail: prime the antiforgery cookie on module load +let csrfPrimed = false; +async function ensureCsrfCookie(): Promise { + if (csrfPrimed || LISTENARR_API_KEY) return; + try { + await listenarrApi.get("/api/v1/system/status"); + csrfPrimed = true; + } catch { + // ignore — we just need the cookie set + } +} export async function searchAudiobooks( query: string @@ -38,10 +58,12 @@ export async function addAudiobook(options: { genres?: string[]; series?: string; seriesNumber?: string; - isbn?: string; + isbn?: string[] | string; explicit?: boolean; abridged?: boolean; }): Promise { + await ensureCsrfCookie(); + // Defensive: ensure series/seriesNumber are always strings, not arrays const series = typeof options.series === "string" ? options.series : ""; const seriesNumber = typeof options.seriesNumber === "string" ? options.seriesNumber : "";