diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..ccc4c6c --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +20.20.2 diff --git a/README.md b/README.md index 7257982..2818ffb 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ The source code is available at [github.com/ChrisAdamsdevelopment/SpectraCleanse For a step-by-step manual validation flow (local, API smoke, auth, billing, upload, cleanse, Docker, and production readiness), see [`docs/manual-qa-checklist.md`](docs/manual-qa-checklist.md). - Browser metadata analysis uses maintained `music-metadata` with graceful fallback (`parseError`) when parsing fails, times out, or is skipped for very large files. -- Quick Cleanse metadata writing remains local/browser-side (MP3 via `browser-id3-writer`), while Full Server Cleanse still runs through `/api/process`. +- Quick Cleanse metadata writing remains local/browser-side (MP3 via `browser-id3-writer`). +- Full Server Cleanse runs through `/api/process` for supported non-MP3 formats; MP3 requests are rejected with HTTP `422` and guidance to use Quick Cleanse Browser. --- @@ -57,6 +58,16 @@ Questions, partnerships, or enterprise enquiries: [hello@spectracleanse.com](mai --- + +## Native Node deployment runtime + +- Native Render/Node deployments should use **Node 20.20.2** (recommended) or a Node version within the supported engines range: `>=18 <23`. +- If Render defaults your service to a newer Node release, set `NODE_VERSION=20.20.2` in the service environment. +- Node 24 is currently not supported for native installs in validation because `better-sqlite3` native compilation failed under Node 24. +- Docker deployments already pin Node 18 via the repo Dockerfile. + +--- + ## Docker production deployment This repository includes a multi-stage `Dockerfile` that builds the frontend and packages `dist/` into the final runtime image so `server.js` can serve the SPA in production. diff --git a/app.tsx b/app.tsx index f1ebca7..54dbcba 100644 --- a/app.tsx +++ b/app.tsx @@ -654,7 +654,7 @@ export default function App() { if (!res.ok) { const errBody = await res.json().catch(() => ({ error: `HTTP ${res.status}` })); - throw new Error(errBody.error || `Server error ${res.status}`); + throw new Error(errBody.detail || errBody.error || `Server error ${res.status}`); } const blob = await res.blob(); diff --git a/docs/manual-qa-checklist.md b/docs/manual-qa-checklist.md index a73d6e6..83381cf 100644 --- a/docs/manual-qa-checklist.md +++ b/docs/manual-qa-checklist.md @@ -98,13 +98,17 @@ Run these checks against your local server: ## 9) Full Server Cleanse QA -1. Run server cleanse on a supported file. +1. Run server cleanse on a supported non-MP3 file. 2. Verify `/api/process` returns downloadable file response. -3. Verify usage meter/counter updates. +3. Verify usage meter/counter updates only after successful delivered files. 4. Verify forensic/report information appears when present. -5. Force or simulate `401` from protected endpoint. +5. Upload MP3 to Full Server Cleanse and verify HTTP `422` JSON: + - Expected error: `MP3 server cleanse is not supported`. + - Expected detail tells user to use Quick Cleanse (Browser) for MP3. + - Expected usage counter does **not** increment on this rejection. +6. Force or simulate `401` from protected endpoint. - Expected: user is logged out/reauth requested. -6. Force or simulate `402`. +7. Force or simulate `402`. - Expected: upgrade modal opens. ## 10) Object URL / download safety QA diff --git a/package-lock.json b/package-lock.json index 74d1257..bb6d6e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "vite": "^4.5.14" }, "engines": { - "node": ">=18" + "node": ">=18 <23" } }, "node_modules/@alloc/quick-lru": { diff --git a/package.json b/package.json index 50cd751..74ee96f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "SpectraCleanse AI", "main": "server.js", "engines": { - "node": ">=18" + "node": ">=18 <23" }, "scripts": { "start": "node server.js", diff --git a/server.js b/server.js index 9fda7da..f28fcbb 100644 --- a/server.js +++ b/server.js @@ -451,6 +451,19 @@ app.post('/api/process', requireAuth, upload.single('file'), async (req, res) => if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); const userId = req.user.sub; + const inputPath = req.file.path; + const originalName = req.file.originalname || ''; + const ext = path.extname(originalName).toLowerCase() || '.mp3'; + const mime = (req.file.mimetype || '').toLowerCase(); + const isMp3 = ext === '.mp3' || mime === 'audio/mpeg'; + + if (isMp3) { + await fs.remove(inputPath).catch(() => {}); + return res.status(422).json({ + error: 'MP3 server cleanse is not supported', + detail: 'Use Quick Cleanse (Browser) for MP3 metadata rewriting, or upload MP4/M4A/WAV/FLAC for Full Server Cleanse.', + }); + } // ── Tier-based usage enforcement ───────────────────────────────────────── // Always re-read plan from DB so upgrades (via webhook) take effect @@ -474,8 +487,6 @@ app.post('/api/process', requireAuth, upload.single('file'), async (req, res) => // ── End enforcement ─────────────────────────────────────────────────────── const { title, description, tags, artist, genre, lyrics, platform = 'General' } = req.body; - const inputPath = req.file.path; - const ext = path.extname(req.file.originalname).toLowerCase() || '.mp3'; const outputPath = path.join('uploads', `out_${Date.now()}${ext}`); try { @@ -490,16 +501,21 @@ app.post('/api/process', requireAuth, upload.single('file'), async (req, res) => const beforeTags = await exiftool.read(outputPath); const beforeKeys = new Set(Object.keys(beforeTags)); - // Phase 2: Nuclear wipe + // Phase 2: Nuclear wipe (supported exiftool-vendored path only) try { - await exiftool.execute('-all=', '-XMP:all=', '-IPTC:all=', '-overwrite_original', outputPath); - } catch (wipeErr) { - console.warn('Primary metadata wipe failed, retrying with exiftool.write fallback:', wipeErr.message); await exiftool.write( outputPath, {}, ['-all=', '-XMP:all=', '-IPTC:all=', '-overwrite_original'] ); + } catch (wipeErr) { + console.warn('Primary metadata wipe failed:', wipeErr.message); + await fs.remove(inputPath).catch(() => {}); + await fs.remove(outputPath).catch(() => {}); + return res.status(422).json({ + error: 'Server cleanse unsupported for this format', + detail: 'This file format cannot be safely metadata-wiped on the server. Use Quick Cleanse (Browser) for MP3 or try MP4/M4A/WAV/FLAC for Full Server Cleanse.', + }); } // Phase 3: Platform-aware SEO injection