From 6048a866623a99b82d584b0f58e1d2469e898b0c Mon Sep 17 00:00:00 2001 From: Jim Carucci <96802642+uncleJim21@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:44:21 -0500 Subject: [PATCH] Guarantee cached audio host via a global response filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Endpoints that dump raw metadataRaw (episode-with-chapters, fetch-adjacent-paragraphs, +5 more) still emitted the raw DigitalOcean host in audioUrl, bypassing the Cloudflare cache — per-key wrapping (#133/#134) can't catch audioUrl nested inside dumped metadata. Add rewriteAudioUrlsDeep() + a res.json middleware that deep-rewrites every audioUrl field in every JSON response to audio.pullthatupjamie.ai. Idempotent, no-ops on non-bucket/podcaster/non-string values. SSE (res.write) unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- server.js | 12 +++++++++++- utils/audioFormat.js | 28 +++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index 89de3ef..c09af11 100644 --- a/server.js +++ b/server.js @@ -26,7 +26,7 @@ const callIngestor = require('./utils/callIngestor'); const path = require('path'); const {DEBUG_MODE, SCHEDULER_ENABLED, SCHEDULED_INGESTOR_TIMES, printLog} = require('./constants.js') const ClipUtils = require('./utils/ClipUtils'); -const { AUDIO_EXTENSIONS, storageKeyFromUrl } = require('./utils/audioFormat'); +const { AUDIO_EXTENSIONS, storageKeyFromUrl, rewriteAudioUrlsDeep } = require('./utils/audioFormat'); const { getPodcastFeed } = require('./utils/LandingPageService'); const {WorkProductV2, calculateLookupHash} = require('./models/WorkProductV2') const QueueJob = require('./models/QueueJob'); @@ -227,6 +227,16 @@ app.use(cors(corsOptions)); app.enable('trust proxy'); app.set('trust proxy', true); app.use(express.json()); + +// Rewrite any audioUrl in JSON responses to the Cloudflare-cached host, so no +// endpoint — including ones that dump raw metadataRaw — hands agents/clients the +// raw DigitalOcean origin (which bypasses the cache). Idempotent; no-ops on +// non-bucket URLs. SSE endpoints use res.write and are unaffected. +app.use((req, res, next) => { + const sendJson = res.json.bind(res); + res.json = (body) => sendJson(rewriteAudioUrlsDeep(body)); + next(); +}); app.use(cookieParser()); // Add this line before session middleware // Add session middleware diff --git a/utils/audioFormat.js b/utils/audioFormat.js index 816b70c..ef84cff 100644 --- a/utils/audioFormat.js +++ b/utils/audioFormat.js @@ -49,4 +49,30 @@ function publicAudioUrl(url) { return url.replace(SPACES_AUDIO_HOST_RE, PUBLIC_AUDIO_HOST); } -module.exports = { AUDIO_EXTENSIONS, storageKeyFromUrl, publicAudioUrl, PUBLIC_AUDIO_HOST }; +/** + * Recursively rewrite every `audioUrl` string field within a value (in place) to + * the public Cloudflare host. Catches audioUrl carried inside dumped `metadataRaw` + * objects / arrays that per-call-site wrapping misses. No-ops on non-bucket URLs + * and non-string values, so it's safe to run over any JSON response body. + * + * @param {*} value + * @returns {*} the same value, mutated in place + */ +function rewriteAudioUrlsDeep(value) { + if (!value || typeof value !== 'object') return value; + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) rewriteAudioUrlsDeep(value[i]); + return value; + } + for (const key of Object.keys(value)) { + const v = value[key]; + if (key === 'audioUrl' && typeof v === 'string') { + value[key] = publicAudioUrl(v); + } else if (v && typeof v === 'object') { + rewriteAudioUrlsDeep(v); + } + } + return value; +} + +module.exports = { AUDIO_EXTENSIONS, storageKeyFromUrl, publicAudioUrl, PUBLIC_AUDIO_HOST, rewriteAudioUrlsDeep };