agentvibes 4.2.0 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agentvibes/bmad/bmad-voices.md +69 -69
- package/.agentvibes/config.json +12 -0
- package/.claude/activation-instructions +54 -54
- package/.claude/audio/tracks/README.md +52 -52
- package/.claude/commands/agent-vibes/add.md +21 -21
- package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
- package/.claude/commands/agent-vibes/agent.md +79 -79
- package/.claude/commands/agent-vibes/background-music.md +111 -111
- package/.claude/commands/agent-vibes/bmad.md +198 -198
- package/.claude/commands/agent-vibes/clean.md +18 -18
- package/.claude/commands/agent-vibes/cleanup.md +18 -18
- package/.claude/commands/agent-vibes/commands.json +145 -145
- package/.claude/commands/agent-vibes/effects.md +97 -97
- package/.claude/commands/agent-vibes/get.md +9 -9
- package/.claude/commands/agent-vibes/hide.md +91 -91
- package/.claude/commands/agent-vibes/language.md +23 -23
- package/.claude/commands/agent-vibes/learn.md +67 -67
- package/.claude/commands/agent-vibes/list.md +13 -13
- package/.claude/commands/agent-vibes/mute.md +37 -37
- package/.claude/commands/agent-vibes/preview.md +17 -17
- package/.claude/commands/agent-vibes/provider.md +68 -68
- package/.claude/commands/agent-vibes/replay-target.md +14 -14
- package/.claude/commands/agent-vibes/sample.md +12 -12
- package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
- package/.claude/commands/agent-vibes/set-pretext.md +65 -65
- package/.claude/commands/agent-vibes/set-speed.md +41 -41
- package/.claude/commands/agent-vibes/show.md +84 -84
- package/.claude/commands/agent-vibes/switch.md +87 -87
- package/.claude/commands/agent-vibes/target-voice.md +26 -26
- package/.claude/commands/agent-vibes/target.md +30 -30
- package/.claude/commands/agent-vibes/translate.md +68 -68
- package/.claude/commands/agent-vibes/unmute.md +45 -45
- package/.claude/commands/agent-vibes/verbosity.md +89 -89
- package/.claude/commands/agent-vibes/whoami.md +7 -7
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/agentvibes.json +1 -0
- package/.claude/config/audio-effects.cfg +2 -2
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-volume.txt +1 -0
- package/.claude/config/intro-text.txt +1 -0
- package/.claude/config/piper-speech-rate.txt +4 -0
- package/.claude/config/piper-target-speech-rate.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +4 -0
- package/.claude/config/tts-target-speech-rate.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/README-TTS-QUEUE.md +135 -135
- package/.claude/hooks/audio-cache-utils.sh +246 -246
- package/.claude/hooks/audio-processor.sh +433 -433
- package/.claude/hooks/background-music-manager.sh +404 -404
- package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
- package/.claude/hooks/bmad-speak.sh +269 -269
- package/.claude/hooks/bmad-tts-injector.sh +568 -568
- package/.claude/hooks/bmad-voice-manager.sh +928 -928
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
- package/.claude/hooks/clawdbot-receiver.sh +107 -107
- package/.claude/hooks/clean-audio-cache.sh +22 -22
- package/.claude/hooks/cleanup-cache.sh +106 -106
- package/.claude/hooks/configure-rdp-mode.sh +137 -137
- package/.claude/hooks/download-extra-voices.sh +244 -244
- package/.claude/hooks/effects-manager.sh +268 -268
- package/.claude/hooks/github-star-reminder.sh +154 -154
- package/.claude/hooks/language-manager.sh +362 -362
- package/.claude/hooks/learn-manager.sh +492 -492
- package/.claude/hooks/macos-voice-manager.sh +205 -205
- package/.claude/hooks/migrate-background-music.sh +125 -125
- package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
- package/.claude/hooks/optimize-background-music.sh +87 -87
- package/.claude/hooks/path-resolver.sh +60 -60
- package/.claude/hooks/personality-manager.sh +448 -448
- package/.claude/hooks/piper-download-voices.sh +225 -225
- package/.claude/hooks/piper-installer.sh +292 -292
- package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
- package/.claude/hooks/piper-voice-manager.sh +24 -3
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
- package/.claude/hooks/play-tts-enhanced.sh +105 -105
- package/.claude/hooks/play-tts-macos.sh +368 -368
- package/.claude/hooks/play-tts-piper.sh +679 -679
- package/.claude/hooks/play-tts-soprano.sh +356 -356
- package/.claude/hooks/play-tts-ssh-remote.sh +167 -167
- package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
- package/.claude/hooks/play-tts.sh +301 -301
- package/.claude/hooks/prepare-release.sh +54 -54
- package/.claude/hooks/provider-commands.sh +617 -617
- package/.claude/hooks/provider-manager.sh +399 -399
- package/.claude/hooks/replay-target-audio.sh +95 -95
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +201 -201
- package/.claude/hooks/session-start-tts.sh +81 -81
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +291 -291
- package/.claude/hooks/stop-tts.sh +84 -84
- package/.claude/hooks/termux-installer.sh +261 -261
- package/.claude/hooks/translate-manager.sh +341 -341
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +145 -145
- package/.claude/hooks/tts-queue.sh +165 -165
- package/.claude/hooks/verbosity-manager.sh +178 -178
- package/.claude/hooks/voice-manager.sh +548 -548
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
- package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
- package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
- package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
- package/.claude/hooks-windows/effects-manager.ps1 +294 -0
- package/.claude/hooks-windows/language-manager.ps1 +193 -0
- package/.claude/hooks-windows/learn-manager.ps1 +241 -0
- package/.claude/hooks-windows/personality-manager.ps1 +266 -0
- package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
- package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
- package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
- package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
- package/.claude/hooks-windows/play-tts.ps1 +344 -266
- package/.claude/hooks-windows/provider-manager.ps1 +29 -10
- package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/speed-manager.ps1 +166 -0
- package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
- package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
- package/.claude/output-styles/agent-vibes.md +202 -202
- package/.claude/personalities/angry.md +14 -14
- package/.claude/personalities/annoying.md +14 -14
- package/.claude/personalities/crass.md +14 -14
- package/.claude/personalities/dramatic.md +14 -14
- package/.claude/personalities/dry-humor.md +50 -50
- package/.claude/personalities/flirty.md +20 -20
- package/.claude/personalities/funny.md +14 -14
- package/.claude/personalities/grandpa.md +32 -32
- package/.claude/personalities/millennial.md +14 -14
- package/.claude/personalities/moody.md +14 -14
- package/.claude/personalities/normal.md +16 -16
- package/.claude/personalities/pirate.md +14 -14
- package/.claude/personalities/poetic.md +14 -14
- package/.claude/personalities/professional.md +14 -14
- package/.claude/personalities/rapper.md +55 -55
- package/.claude/personalities/robot.md +14 -14
- package/.claude/personalities/sarcastic.md +38 -38
- package/.claude/personalities/sassy.md +14 -14
- package/.claude/personalities/surfer-dude.md +14 -14
- package/.claude/personalities/zen.md +14 -14
- package/.claude/settings.json +15 -15
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.clawdbot/skill/SKILL.md +241 -241
- package/.mcp.json +12 -0
- package/CLAUDE.md +170 -170
- package/README.md +2029 -2007
- package/RELEASE_NOTES.md +1310 -1203
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +1840 -1840
- package/bin/agentvibes.js +48 -2
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +206 -206
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/WINDOWS_SETUP.md +260 -260
- package/mcp-server/docs/troubleshooting-audio.md +313 -313
- package/mcp-server/examples/claude_desktop_config.json +11 -11
- package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
- package/mcp-server/examples/custom_instructions.md +169 -169
- package/mcp-server/install-deps.js +130 -130
- package/mcp-server/pyproject.toml +52 -52
- package/mcp-server/requirements.txt +2 -2
- package/mcp-server/server.py +1465 -1453
- package/mcp-server/test_server.py +395 -395
- package/mcp-server/test_windows_script_parity.py +336 -0
- package/package.json +110 -110
- package/setup-windows.ps1 +815 -815
- package/src/bmad-detector.js +71 -71
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +114 -114
- package/src/commands/bmad-voices.js +394 -394
- package/src/commands/install-mcp.js +476 -476
- package/src/console/app.js +824 -824
- package/src/console/audio-env.js +20 -1
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/footer-config.js +50 -50
- package/src/console/modals/modal-overlay.js +247 -247
- package/src/console/navigation.js +62 -62
- package/src/console/tabs/agents-tab.js +1684 -1516
- package/src/console/tabs/help-tab.js +261 -261
- package/src/console/tabs/install-tab.js +1007 -991
- package/src/console/tabs/music-tab.js +22 -8
- package/src/console/tabs/placeholder-tab.js +53 -53
- package/src/console/tabs/readme-tab.js +267 -267
- package/src/console/tabs/receiver-tab.js +1472 -1212
- package/src/console/tabs/settings-tab.js +208 -84
- package/src/console/tabs/voices-tab.js +100 -21
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/format-utils.js +89 -89
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/personality-picker.js +185 -185
- package/src/console/widgets/reverb-picker.js +94 -94
- package/src/console/widgets/track-picker.js +285 -285
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +5895 -5829
- package/src/services/agent-voice-store.js +423 -423
- package/src/services/config-service.js +264 -264
- package/src/services/navigation-service.js +123 -123
- package/src/services/provider-service.js +143 -132
- package/src/services/verbosity-service.js +157 -157
- package/src/utils/audio-duration-validator.js +298 -298
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -466
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/list-formatter.js +194 -194
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +136 -136
- package/src/utils/provider-validator.js +96 -12
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.sh +482 -482
- package/templates/audio/welcome-music.mp3 +0 -0
- package/voice-assignments.json +8244 -8244
- package/.claude/config/background-music-position.txt +0 -1
|
@@ -1,285 +1,285 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Music File Validator - Security-Critical Path and File Validation
|
|
3
|
-
* Story 4.1: Path Validation and Security Hardening
|
|
4
|
-
*
|
|
5
|
-
* CRITICAL: All functions enforce CLAUDE.md security requirements:
|
|
6
|
-
* - Use path.resolve() for all path operations
|
|
7
|
-
* - Verify file ownership before processing external files
|
|
8
|
-
* - Prevent path traversal attacks via traversal patterns, symlinks, etc.
|
|
9
|
-
* - Validate file exists, is readable, and owned by current user
|
|
10
|
-
*
|
|
11
|
-
* @module music-file-validator
|
|
12
|
-
* @requires fs
|
|
13
|
-
* @requires path
|
|
14
|
-
* @requires os
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import fs from 'node:fs';
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
import os from 'node:os';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Story 4.1: Validate that a file path is safe and within user's home directory
|
|
23
|
-
*
|
|
24
|
-
* Security checks:
|
|
25
|
-
* 1. Resolves path to absolute form (prevents ../ tricks)
|
|
26
|
-
* 2. Validates path is within home directory
|
|
27
|
-
* 3. Rejects symlinks pointing outside home
|
|
28
|
-
* 4. Verifies file ownership matches current user
|
|
29
|
-
* 5. Checks file is readable
|
|
30
|
-
*
|
|
31
|
-
* @param {string} userPath - Path provided by user (absolute or relative)
|
|
32
|
-
* @param {string} userHomeDir - User's home directory (default: process.env.HOME)
|
|
33
|
-
* @returns {Object} { isValid: boolean, error: string|null, resolvedPath: string|null }
|
|
34
|
-
* @throws {Error} If path validation encounters unexpected errors
|
|
35
|
-
*/
|
|
36
|
-
export function isPathSafe(userPath, userHomeDir = null) {
|
|
37
|
-
try {
|
|
38
|
-
// Parameter validation
|
|
39
|
-
if (!userPath || typeof userPath !== 'string') {
|
|
40
|
-
return {
|
|
41
|
-
isValid: false,
|
|
42
|
-
error: 'Path must be a non-empty string',
|
|
43
|
-
resolvedPath: null
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Use provided home dir or default to environment
|
|
48
|
-
const homeDir = userHomeDir || (process.env.HOME || process.env.USERPROFILE);
|
|
49
|
-
if (!homeDir) {
|
|
50
|
-
return {
|
|
51
|
-
isValid: false,
|
|
52
|
-
error: 'Unable to determine home directory',
|
|
53
|
-
resolvedPath: null
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// CRITICAL: Use path.resolve() to get absolute path (CLAUDE.md requirement)
|
|
58
|
-
// This prevents directory traversal attacks like "../../etc/passwd"
|
|
59
|
-
const resolvedPath = path.resolve(userPath);
|
|
60
|
-
const resolvedHome = path.resolve(homeDir);
|
|
61
|
-
|
|
62
|
-
// Check if path is within home directory
|
|
63
|
-
// Must either match exactly OR start with home + separator (prevents /home/user2 bypass)
|
|
64
|
-
const isWithinHome = resolvedPath === resolvedHome ||
|
|
65
|
-
resolvedPath.startsWith(resolvedHome + path.sep);
|
|
66
|
-
|
|
67
|
-
if (!isWithinHome) {
|
|
68
|
-
return {
|
|
69
|
-
isValid: false,
|
|
70
|
-
error: `Security validation failed: path must be within home directory (${resolvedHome})`,
|
|
71
|
-
resolvedPath: null
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// SECURITY: Use lstatSync first to detect symlinks before following them (#131)
|
|
76
|
-
let lstats;
|
|
77
|
-
try {
|
|
78
|
-
lstats = fs.lstatSync(resolvedPath);
|
|
79
|
-
} catch (err) {
|
|
80
|
-
if (err.code === 'ENOENT') {
|
|
81
|
-
return { isValid: false, error: `File not found: ${userPath}`, resolvedPath: null };
|
|
82
|
-
}
|
|
83
|
-
return { isValid: false, error: `Cannot access file: ${err.message}`, resolvedPath: null };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Check if symlink - if so, verify target is also within home directory
|
|
87
|
-
if (lstats.isSymbolicLink()) {
|
|
88
|
-
try {
|
|
89
|
-
const targetPath = fs.realpathSync(resolvedPath);
|
|
90
|
-
const isTargetWithinHome = targetPath === resolvedHome ||
|
|
91
|
-
targetPath.startsWith(resolvedHome + path.sep);
|
|
92
|
-
|
|
93
|
-
if (!isTargetWithinHome) {
|
|
94
|
-
return {
|
|
95
|
-
isValid: false,
|
|
96
|
-
error: 'Security validation failed: symlink target must be within home directory',
|
|
97
|
-
resolvedPath: null
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
} catch (err) {
|
|
101
|
-
return {
|
|
102
|
-
isValid: false,
|
|
103
|
-
error: `Failed to resolve symlink: ${err.message}`,
|
|
104
|
-
resolvedPath: null
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Get file stats (follows symlinks for regular file check)
|
|
110
|
-
const stats = fs.statSync(resolvedPath);
|
|
111
|
-
|
|
112
|
-
// Check if it's a regular file (not directory, special file, etc)
|
|
113
|
-
if (!stats.isFile()) {
|
|
114
|
-
return {
|
|
115
|
-
isValid: false,
|
|
116
|
-
error: `Path must be a regular file, not a ${stats.isDirectory() ? 'directory' : 'special file'}`,
|
|
117
|
-
resolvedPath: null
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// CRITICAL: Verify file ownership (CLAUDE.md requirement)
|
|
122
|
-
// Prevent other users from planting malicious files
|
|
123
|
-
// SECURITY: Fail-secure on platforms where getuid is unavailable (#131)
|
|
124
|
-
const currentUserId = process.getuid ? process.getuid() : null;
|
|
125
|
-
if (currentUserId === null) {
|
|
126
|
-
return {
|
|
127
|
-
isValid: false,
|
|
128
|
-
error: 'Security validation failed: unable to verify file ownership on this platform',
|
|
129
|
-
resolvedPath: null
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
if (stats.uid !== currentUserId) {
|
|
133
|
-
return {
|
|
134
|
-
isValid: false,
|
|
135
|
-
error: 'Security validation failed: file not owned by current user',
|
|
136
|
-
resolvedPath: null
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Check if file is readable
|
|
141
|
-
try {
|
|
142
|
-
fs.accessSync(resolvedPath, fs.constants.R_OK);
|
|
143
|
-
} catch (err) {
|
|
144
|
-
return {
|
|
145
|
-
isValid: false,
|
|
146
|
-
error: `File is not readable: ${err.message}`,
|
|
147
|
-
resolvedPath: null
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// All checks passed
|
|
152
|
-
return {
|
|
153
|
-
isValid: true,
|
|
154
|
-
error: null,
|
|
155
|
-
resolvedPath
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
} catch (err) {
|
|
159
|
-
// Unexpected error during validation
|
|
160
|
-
return {
|
|
161
|
-
isValid: false,
|
|
162
|
-
error: `Unexpected validation error: ${err.message}`,
|
|
163
|
-
resolvedPath: null
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Verify file size is within acceptable limits for music files
|
|
170
|
-
*
|
|
171
|
-
* @param {string} filePath - Path to file (must already be validated with isPathSafe)
|
|
172
|
-
* @param {number} maxSizeBytes - Maximum file size in bytes (default: 50MB)
|
|
173
|
-
* @returns {Object} { isValid: boolean, error: string|null, sizeBytes: number }
|
|
174
|
-
*/
|
|
175
|
-
export function validateFileSize(filePath, maxSizeBytes = 50 * 1024 * 1024) {
|
|
176
|
-
try {
|
|
177
|
-
if (!fs.existsSync(filePath)) {
|
|
178
|
-
return {
|
|
179
|
-
isValid: false,
|
|
180
|
-
error: 'File does not exist',
|
|
181
|
-
sizeBytes: 0
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const stats = fs.statSync(filePath);
|
|
186
|
-
const sizeBytes = stats.size;
|
|
187
|
-
|
|
188
|
-
if (sizeBytes > maxSizeBytes) {
|
|
189
|
-
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
|
|
190
|
-
const maxMB = (maxSizeBytes / 1024 / 1024).toFixed(2);
|
|
191
|
-
return {
|
|
192
|
-
isValid: false,
|
|
193
|
-
error: `File size (${sizeMB}MB) exceeds maximum (${maxMB}MB)`,
|
|
194
|
-
sizeBytes
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (sizeBytes === 0) {
|
|
199
|
-
return {
|
|
200
|
-
isValid: false,
|
|
201
|
-
error: 'File is empty',
|
|
202
|
-
sizeBytes
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
isValid: true,
|
|
208
|
-
error: null,
|
|
209
|
-
sizeBytes
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
} catch (err) {
|
|
213
|
-
return {
|
|
214
|
-
isValid: false,
|
|
215
|
-
error: `Error checking file size: ${err.message}`,
|
|
216
|
-
sizeBytes: 0
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Get secure temp directory for audio file operations
|
|
223
|
-
* Uses XDG_RUNTIME_DIR if available (follows CLAUDE.md requirement)
|
|
224
|
-
* Falls back to user-specific /tmp directory
|
|
225
|
-
*
|
|
226
|
-
* @param {string} prefix - Directory name prefix (default: 'agentvibes-music')
|
|
227
|
-
* @returns {string} Secure temp directory path
|
|
228
|
-
*/
|
|
229
|
-
export function getSecureTempDir(prefix = 'agentvibes-music') {
|
|
230
|
-
const xdgRuntime = process.env.XDG_RUNTIME_DIR;
|
|
231
|
-
|
|
232
|
-
if (xdgRuntime && fs.existsSync(xdgRuntime)) {
|
|
233
|
-
return path.join(xdgRuntime, `${prefix}-${process.pid}`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Fallback to user-specific /tmp
|
|
237
|
-
const userTmp = path.join(os.tmpdir(), `${prefix}-${process.env.USER || 'user'}`);
|
|
238
|
-
return userTmp;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Create secure temp directory with restrictive permissions
|
|
243
|
-
*
|
|
244
|
-
* @param {string} dirPath - Directory path to create
|
|
245
|
-
* @returns {Object} { success: boolean, error: string|null, dirPath: string|null }
|
|
246
|
-
*/
|
|
247
|
-
export function createSecureTempDir(dirPath) {
|
|
248
|
-
try {
|
|
249
|
-
// Create directory if it doesn't exist
|
|
250
|
-
if (!fs.existsSync(dirPath)) {
|
|
251
|
-
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
252
|
-
} else {
|
|
253
|
-
// If exists, verify permissions are restrictive
|
|
254
|
-
const stats = fs.statSync(dirPath);
|
|
255
|
-
// Check if world-readable (mode & 0o077 should be 0 for secure)
|
|
256
|
-
if ((stats.mode & 0o077) !== 0) {
|
|
257
|
-
return {
|
|
258
|
-
success: false,
|
|
259
|
-
error: 'Temp directory has insecure permissions',
|
|
260
|
-
dirPath: null
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return {
|
|
266
|
-
success: true,
|
|
267
|
-
error: null,
|
|
268
|
-
dirPath
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
} catch (err) {
|
|
272
|
-
return {
|
|
273
|
-
success: false,
|
|
274
|
-
error: `Failed to create secure temp directory: ${err.message}`,
|
|
275
|
-
dirPath: null
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export default {
|
|
281
|
-
isPathSafe,
|
|
282
|
-
validateFileSize,
|
|
283
|
-
getSecureTempDir,
|
|
284
|
-
createSecureTempDir
|
|
285
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Music File Validator - Security-Critical Path and File Validation
|
|
3
|
+
* Story 4.1: Path Validation and Security Hardening
|
|
4
|
+
*
|
|
5
|
+
* CRITICAL: All functions enforce CLAUDE.md security requirements:
|
|
6
|
+
* - Use path.resolve() for all path operations
|
|
7
|
+
* - Verify file ownership before processing external files
|
|
8
|
+
* - Prevent path traversal attacks via traversal patterns, symlinks, etc.
|
|
9
|
+
* - Validate file exists, is readable, and owned by current user
|
|
10
|
+
*
|
|
11
|
+
* @module music-file-validator
|
|
12
|
+
* @requires fs
|
|
13
|
+
* @requires path
|
|
14
|
+
* @requires os
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Story 4.1: Validate that a file path is safe and within user's home directory
|
|
23
|
+
*
|
|
24
|
+
* Security checks:
|
|
25
|
+
* 1. Resolves path to absolute form (prevents ../ tricks)
|
|
26
|
+
* 2. Validates path is within home directory
|
|
27
|
+
* 3. Rejects symlinks pointing outside home
|
|
28
|
+
* 4. Verifies file ownership matches current user
|
|
29
|
+
* 5. Checks file is readable
|
|
30
|
+
*
|
|
31
|
+
* @param {string} userPath - Path provided by user (absolute or relative)
|
|
32
|
+
* @param {string} userHomeDir - User's home directory (default: process.env.HOME)
|
|
33
|
+
* @returns {Object} { isValid: boolean, error: string|null, resolvedPath: string|null }
|
|
34
|
+
* @throws {Error} If path validation encounters unexpected errors
|
|
35
|
+
*/
|
|
36
|
+
export function isPathSafe(userPath, userHomeDir = null) {
|
|
37
|
+
try {
|
|
38
|
+
// Parameter validation
|
|
39
|
+
if (!userPath || typeof userPath !== 'string') {
|
|
40
|
+
return {
|
|
41
|
+
isValid: false,
|
|
42
|
+
error: 'Path must be a non-empty string',
|
|
43
|
+
resolvedPath: null
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Use provided home dir or default to environment
|
|
48
|
+
const homeDir = userHomeDir || (process.env.HOME || process.env.USERPROFILE);
|
|
49
|
+
if (!homeDir) {
|
|
50
|
+
return {
|
|
51
|
+
isValid: false,
|
|
52
|
+
error: 'Unable to determine home directory',
|
|
53
|
+
resolvedPath: null
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// CRITICAL: Use path.resolve() to get absolute path (CLAUDE.md requirement)
|
|
58
|
+
// This prevents directory traversal attacks like "../../etc/passwd"
|
|
59
|
+
const resolvedPath = path.resolve(userPath);
|
|
60
|
+
const resolvedHome = path.resolve(homeDir);
|
|
61
|
+
|
|
62
|
+
// Check if path is within home directory
|
|
63
|
+
// Must either match exactly OR start with home + separator (prevents /home/user2 bypass)
|
|
64
|
+
const isWithinHome = resolvedPath === resolvedHome ||
|
|
65
|
+
resolvedPath.startsWith(resolvedHome + path.sep);
|
|
66
|
+
|
|
67
|
+
if (!isWithinHome) {
|
|
68
|
+
return {
|
|
69
|
+
isValid: false,
|
|
70
|
+
error: `Security validation failed: path must be within home directory (${resolvedHome})`,
|
|
71
|
+
resolvedPath: null
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// SECURITY: Use lstatSync first to detect symlinks before following them (#131)
|
|
76
|
+
let lstats;
|
|
77
|
+
try {
|
|
78
|
+
lstats = fs.lstatSync(resolvedPath);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err.code === 'ENOENT') {
|
|
81
|
+
return { isValid: false, error: `File not found: ${userPath}`, resolvedPath: null };
|
|
82
|
+
}
|
|
83
|
+
return { isValid: false, error: `Cannot access file: ${err.message}`, resolvedPath: null };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check if symlink - if so, verify target is also within home directory
|
|
87
|
+
if (lstats.isSymbolicLink()) {
|
|
88
|
+
try {
|
|
89
|
+
const targetPath = fs.realpathSync(resolvedPath);
|
|
90
|
+
const isTargetWithinHome = targetPath === resolvedHome ||
|
|
91
|
+
targetPath.startsWith(resolvedHome + path.sep);
|
|
92
|
+
|
|
93
|
+
if (!isTargetWithinHome) {
|
|
94
|
+
return {
|
|
95
|
+
isValid: false,
|
|
96
|
+
error: 'Security validation failed: symlink target must be within home directory',
|
|
97
|
+
resolvedPath: null
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return {
|
|
102
|
+
isValid: false,
|
|
103
|
+
error: `Failed to resolve symlink: ${err.message}`,
|
|
104
|
+
resolvedPath: null
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Get file stats (follows symlinks for regular file check)
|
|
110
|
+
const stats = fs.statSync(resolvedPath);
|
|
111
|
+
|
|
112
|
+
// Check if it's a regular file (not directory, special file, etc)
|
|
113
|
+
if (!stats.isFile()) {
|
|
114
|
+
return {
|
|
115
|
+
isValid: false,
|
|
116
|
+
error: `Path must be a regular file, not a ${stats.isDirectory() ? 'directory' : 'special file'}`,
|
|
117
|
+
resolvedPath: null
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// CRITICAL: Verify file ownership (CLAUDE.md requirement)
|
|
122
|
+
// Prevent other users from planting malicious files
|
|
123
|
+
// SECURITY: Fail-secure on platforms where getuid is unavailable (#131)
|
|
124
|
+
const currentUserId = process.getuid ? process.getuid() : null;
|
|
125
|
+
if (currentUserId === null) {
|
|
126
|
+
return {
|
|
127
|
+
isValid: false,
|
|
128
|
+
error: 'Security validation failed: unable to verify file ownership on this platform',
|
|
129
|
+
resolvedPath: null
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (stats.uid !== currentUserId) {
|
|
133
|
+
return {
|
|
134
|
+
isValid: false,
|
|
135
|
+
error: 'Security validation failed: file not owned by current user',
|
|
136
|
+
resolvedPath: null
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check if file is readable
|
|
141
|
+
try {
|
|
142
|
+
fs.accessSync(resolvedPath, fs.constants.R_OK);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
return {
|
|
145
|
+
isValid: false,
|
|
146
|
+
error: `File is not readable: ${err.message}`,
|
|
147
|
+
resolvedPath: null
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// All checks passed
|
|
152
|
+
return {
|
|
153
|
+
isValid: true,
|
|
154
|
+
error: null,
|
|
155
|
+
resolvedPath
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
} catch (err) {
|
|
159
|
+
// Unexpected error during validation
|
|
160
|
+
return {
|
|
161
|
+
isValid: false,
|
|
162
|
+
error: `Unexpected validation error: ${err.message}`,
|
|
163
|
+
resolvedPath: null
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Verify file size is within acceptable limits for music files
|
|
170
|
+
*
|
|
171
|
+
* @param {string} filePath - Path to file (must already be validated with isPathSafe)
|
|
172
|
+
* @param {number} maxSizeBytes - Maximum file size in bytes (default: 50MB)
|
|
173
|
+
* @returns {Object} { isValid: boolean, error: string|null, sizeBytes: number }
|
|
174
|
+
*/
|
|
175
|
+
export function validateFileSize(filePath, maxSizeBytes = 50 * 1024 * 1024) {
|
|
176
|
+
try {
|
|
177
|
+
if (!fs.existsSync(filePath)) {
|
|
178
|
+
return {
|
|
179
|
+
isValid: false,
|
|
180
|
+
error: 'File does not exist',
|
|
181
|
+
sizeBytes: 0
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const stats = fs.statSync(filePath);
|
|
186
|
+
const sizeBytes = stats.size;
|
|
187
|
+
|
|
188
|
+
if (sizeBytes > maxSizeBytes) {
|
|
189
|
+
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
|
|
190
|
+
const maxMB = (maxSizeBytes / 1024 / 1024).toFixed(2);
|
|
191
|
+
return {
|
|
192
|
+
isValid: false,
|
|
193
|
+
error: `File size (${sizeMB}MB) exceeds maximum (${maxMB}MB)`,
|
|
194
|
+
sizeBytes
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (sizeBytes === 0) {
|
|
199
|
+
return {
|
|
200
|
+
isValid: false,
|
|
201
|
+
error: 'File is empty',
|
|
202
|
+
sizeBytes
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
isValid: true,
|
|
208
|
+
error: null,
|
|
209
|
+
sizeBytes
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
} catch (err) {
|
|
213
|
+
return {
|
|
214
|
+
isValid: false,
|
|
215
|
+
error: `Error checking file size: ${err.message}`,
|
|
216
|
+
sizeBytes: 0
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get secure temp directory for audio file operations
|
|
223
|
+
* Uses XDG_RUNTIME_DIR if available (follows CLAUDE.md requirement)
|
|
224
|
+
* Falls back to user-specific /tmp directory
|
|
225
|
+
*
|
|
226
|
+
* @param {string} prefix - Directory name prefix (default: 'agentvibes-music')
|
|
227
|
+
* @returns {string} Secure temp directory path
|
|
228
|
+
*/
|
|
229
|
+
export function getSecureTempDir(prefix = 'agentvibes-music') {
|
|
230
|
+
const xdgRuntime = process.env.XDG_RUNTIME_DIR;
|
|
231
|
+
|
|
232
|
+
if (xdgRuntime && fs.existsSync(xdgRuntime)) {
|
|
233
|
+
return path.join(xdgRuntime, `${prefix}-${process.pid}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Fallback to user-specific /tmp
|
|
237
|
+
const userTmp = path.join(os.tmpdir(), `${prefix}-${process.env.USER || 'user'}`);
|
|
238
|
+
return userTmp;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Create secure temp directory with restrictive permissions
|
|
243
|
+
*
|
|
244
|
+
* @param {string} dirPath - Directory path to create
|
|
245
|
+
* @returns {Object} { success: boolean, error: string|null, dirPath: string|null }
|
|
246
|
+
*/
|
|
247
|
+
export function createSecureTempDir(dirPath) {
|
|
248
|
+
try {
|
|
249
|
+
// Create directory if it doesn't exist
|
|
250
|
+
if (!fs.existsSync(dirPath)) {
|
|
251
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
252
|
+
} else {
|
|
253
|
+
// If exists, verify permissions are restrictive
|
|
254
|
+
const stats = fs.statSync(dirPath);
|
|
255
|
+
// Check if world-readable (mode & 0o077 should be 0 for secure)
|
|
256
|
+
if ((stats.mode & 0o077) !== 0) {
|
|
257
|
+
return {
|
|
258
|
+
success: false,
|
|
259
|
+
error: 'Temp directory has insecure permissions',
|
|
260
|
+
dirPath: null
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
success: true,
|
|
267
|
+
error: null,
|
|
268
|
+
dirPath
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
} catch (err) {
|
|
272
|
+
return {
|
|
273
|
+
success: false,
|
|
274
|
+
error: `Failed to create secure temp directory: ${err.message}`,
|
|
275
|
+
dirPath: null
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export default {
|
|
281
|
+
isPathSafe,
|
|
282
|
+
validateFileSize,
|
|
283
|
+
getSecureTempDir,
|
|
284
|
+
createSecureTempDir
|
|
285
|
+
};
|