Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20.20.2
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
12 changes: 8 additions & 4 deletions docs/manual-qa-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (typo): Rephrase "successful delivered files" to improve grammar and clarity.

Consider rephrasing to something like "updates only after files are successfully delivered" or "updates only after successful file delivery" for clearer, grammatical wording.

Suggested change
3. Verify usage meter/counter updates only after successful delivered files.
3. Verify usage meter/counter updates only after files are successfully delivered.

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
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "SpectraCleanse AI",
"main": "server.js",
"engines": {
"node": ">=18"
"node": ">=18 <23"
},
"scripts": {
"start": "node server.js",
Expand Down
28 changes: 22 additions & 6 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Comment on lines +455 to +458
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Defaulting unknown extensions to .mp3 risks misclassification and mismatched output extensions.

When originalname is empty or has no extension, path.extname returns '', so ext becomes .mp3. For non‑MP3 uploads without a usable filename, this can result in isMp3 === false (based on MIME) but an .mp3 output extension, and in other supported formats being rejected because they’re treated as .mp3. Consider deriving ext from mimetype when originalname is unusable (e.g., audio/wav.wav, audio/flac.flac), and only then falling back to a neutral extension rather than .mp3, to avoid reintroducing MP3‑specific assumptions.


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
Expand All @@ -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 {
Expand All @@ -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
Expand Down
Loading