@girardmedia/bootspring 2.2.0 → 2.2.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/bin/bootspring.js +127 -73
- package/claude-commands/agent.md +34 -0
- package/claude-commands/bs.md +31 -0
- package/claude-commands/build.md +25 -0
- package/claude-commands/skill.md +31 -0
- package/claude-commands/todo.md +25 -0
- package/dist/core/index.d.ts +5814 -0
- package/dist/core.js +5779 -0
- package/dist/index.js +93883 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp-server.js +2298 -0
- package/package.json +22 -55
- package/core/api-client.d.ts +0 -69
- package/core/api-client.js +0 -1482
- package/core/auth.d.ts +0 -98
- package/core/auth.js +0 -737
- package/core/build-orchestrator.js +0 -508
- package/core/build-state.js +0 -612
- package/core/config.d.ts +0 -106
- package/core/config.js +0 -1328
- package/core/context-loader.js +0 -580
- package/core/context.d.ts +0 -61
- package/core/context.js +0 -327
- package/core/entitlements.d.ts +0 -70
- package/core/entitlements.js +0 -322
- package/core/index.d.ts +0 -53
- package/core/index.js +0 -62
- package/core/mcp-config.js +0 -115
- package/core/policies.d.ts +0 -43
- package/core/policies.js +0 -113
- package/core/policy-matrix.js +0 -303
- package/core/project-activity.js +0 -175
- package/core/redaction.d.ts +0 -5
- package/core/redaction.js +0 -63
- package/core/self-update.js +0 -259
- package/core/session.js +0 -353
- package/core/task-extractor.js +0 -1098
- package/core/telemetry.d.ts +0 -55
- package/core/telemetry.js +0 -617
- package/core/tier-enforcement.js +0 -928
- package/core/utils.d.ts +0 -90
- package/core/utils.js +0 -455
- package/core/validation.js +0 -572
- package/mcp/server.d.ts +0 -57
- package/mcp/server.js +0 -264
package/core/validation.js
DELETED
|
@@ -1,572 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI Input Validation Module
|
|
3
|
-
* Comprehensive validation for CLI arguments and user input.
|
|
4
|
-
*
|
|
5
|
-
* @module core/validation
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Default limits for validation
|
|
13
|
-
*/
|
|
14
|
-
const LIMITS = {
|
|
15
|
-
MAX_STRING_LENGTH: 1000,
|
|
16
|
-
MAX_PATH_LENGTH: 4096,
|
|
17
|
-
MAX_FILENAME_LENGTH: 255,
|
|
18
|
-
MAX_TODO_LENGTH: 500,
|
|
19
|
-
MAX_PROJECT_NAME_LENGTH: 100,
|
|
20
|
-
MAX_DESCRIPTION_LENGTH: 2000,
|
|
21
|
-
MIN_ID: 1,
|
|
22
|
-
MAX_ID: Number.MAX_SAFE_INTEGER,
|
|
23
|
-
MAX_ARRAY_LENGTH: 1000
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Patterns for validation
|
|
28
|
-
*/
|
|
29
|
-
const PATTERNS = {
|
|
30
|
-
// Safe project names: alphanumeric, hyphens, underscores
|
|
31
|
-
PROJECT_NAME: /^[a-zA-Z][a-zA-Z0-9_-]*$/,
|
|
32
|
-
// Safe filenames: path separators and null bytes are checked separately
|
|
33
|
-
SAFE_FILENAME: /^[^:*?"<>|]+$/,
|
|
34
|
-
// Slug format: lowercase, hyphens only
|
|
35
|
-
SLUG: /^[a-z][a-z0-9-]*$/,
|
|
36
|
-
// Numeric ID
|
|
37
|
-
NUMERIC_ID: /^\d+$/,
|
|
38
|
-
// Email format (basic)
|
|
39
|
-
EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
40
|
-
// URL format (basic)
|
|
41
|
-
URL: /^https?:\/\/[^\s]+$/
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Characters that should be escaped or rejected in shell contexts
|
|
46
|
-
*/
|
|
47
|
-
const SHELL_DANGEROUS_CHARS = /[;&|`$(){}[\]<>\\!#*?"'\n\r\t]/;
|
|
48
|
-
|
|
49
|
-
function stripUnsafeControlChars(value, options = {}) {
|
|
50
|
-
const { allowTabs = false, allowNewlines = false } = options;
|
|
51
|
-
let sanitized = '';
|
|
52
|
-
|
|
53
|
-
for (const char of value) {
|
|
54
|
-
const code = char.charCodeAt(0);
|
|
55
|
-
const isLineBreak = code === 0x0A || code === 0x0D;
|
|
56
|
-
const isTab = code === 0x09;
|
|
57
|
-
const isControl = (code >= 0x00 && code <= 0x1F) || code === 0x7F;
|
|
58
|
-
|
|
59
|
-
if (!isControl) {
|
|
60
|
-
sanitized += char;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (isLineBreak && allowNewlines) {
|
|
65
|
-
sanitized += char;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (isTab && allowTabs) {
|
|
70
|
-
sanitized += char;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return sanitized;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Validate string length
|
|
79
|
-
* @param {string} value - Value to validate
|
|
80
|
-
* @param {number} [maxLength] - Maximum length (default: 1000)
|
|
81
|
-
* @param {number} [minLength] - Minimum length (default: 0)
|
|
82
|
-
* @returns {{ valid: boolean, error?: string, sanitized?: string }}
|
|
83
|
-
*/
|
|
84
|
-
function validateStringLength(value, maxLength = LIMITS.MAX_STRING_LENGTH, minLength = 0) {
|
|
85
|
-
if (typeof value !== 'string') {
|
|
86
|
-
return { valid: false, error: 'Value must be a string' };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (value.length < minLength) {
|
|
90
|
-
return { valid: false, error: `String must be at least ${minLength} characters` };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (value.length > maxLength) {
|
|
94
|
-
return {
|
|
95
|
-
valid: false,
|
|
96
|
-
error: `String exceeds maximum length of ${maxLength} characters`,
|
|
97
|
-
sanitized: value.slice(0, maxLength)
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return { valid: true, sanitized: value };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Validate and parse numeric ID
|
|
106
|
-
* @param {string|number} value - Value to validate
|
|
107
|
-
* @param {object} [options] - Options
|
|
108
|
-
* @param {number} [options.min] - Minimum value
|
|
109
|
-
* @param {number} [options.max] - Maximum value
|
|
110
|
-
* @returns {{ valid: boolean, error?: string, value?: number }}
|
|
111
|
-
*/
|
|
112
|
-
function validateNumericId(value, options = {}) {
|
|
113
|
-
const { min = LIMITS.MIN_ID, max = LIMITS.MAX_ID } = options;
|
|
114
|
-
|
|
115
|
-
// Handle string input
|
|
116
|
-
if (typeof value === 'string') {
|
|
117
|
-
if (!PATTERNS.NUMERIC_ID.test(value.trim())) {
|
|
118
|
-
return { valid: false, error: 'Invalid numeric ID format' };
|
|
119
|
-
}
|
|
120
|
-
value = parseInt(value.trim(), 10);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
124
|
-
return { valid: false, error: 'Value must be a finite number' };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!Number.isInteger(value)) {
|
|
128
|
-
return { valid: false, error: 'Value must be an integer' };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (value < min) {
|
|
132
|
-
return { valid: false, error: `Value must be at least ${min}` };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (value > max) {
|
|
136
|
-
return { valid: false, error: `Value must be at most ${max}` };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return { valid: true, value };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Validate project name
|
|
144
|
-
* @param {string} name - Project name to validate
|
|
145
|
-
* @returns {{ valid: boolean, error?: string, sanitized?: string }}
|
|
146
|
-
*/
|
|
147
|
-
function validateProjectName(name) {
|
|
148
|
-
const lengthResult = validateStringLength(name, LIMITS.MAX_PROJECT_NAME_LENGTH, 1);
|
|
149
|
-
if (!lengthResult.valid) {
|
|
150
|
-
return lengthResult;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!PATTERNS.PROJECT_NAME.test(name)) {
|
|
154
|
-
// Try to sanitize
|
|
155
|
-
const sanitized = name
|
|
156
|
-
.toLowerCase()
|
|
157
|
-
.replace(/[^a-z0-9_-]/g, '-')
|
|
158
|
-
.replace(/^[^a-z]+/, '')
|
|
159
|
-
.replace(/-+/g, '-')
|
|
160
|
-
.replace(/-$/, '');
|
|
161
|
-
|
|
162
|
-
if (sanitized.length === 0) {
|
|
163
|
-
return { valid: false, error: 'Project name must start with a letter and contain only alphanumeric characters, hyphens, and underscores' };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return { valid: false, error: 'Invalid characters in project name', sanitized };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return { valid: true, sanitized: name };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Validate filename (no path components)
|
|
174
|
-
* @param {string} filename - Filename to validate
|
|
175
|
-
* @returns {{ valid: boolean, error?: string, sanitized?: string }}
|
|
176
|
-
*/
|
|
177
|
-
function validateFilename(filename) {
|
|
178
|
-
if (typeof filename !== 'string') {
|
|
179
|
-
return { valid: false, error: 'Filename must be a string' };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (filename.length === 0) {
|
|
183
|
-
return { valid: false, error: 'Filename cannot be empty' };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (filename.length > LIMITS.MAX_FILENAME_LENGTH) {
|
|
187
|
-
return { valid: false, error: `Filename exceeds maximum length of ${LIMITS.MAX_FILENAME_LENGTH} characters` };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Check for path separators
|
|
191
|
-
if (filename.includes('/') || filename.includes('\\')) {
|
|
192
|
-
return { valid: false, error: 'Filename cannot contain path separators' };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Check for null bytes
|
|
196
|
-
if (filename.includes('\x00')) {
|
|
197
|
-
return { valid: false, error: 'Filename contains invalid characters' };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Check for reserved names (Windows compatibility)
|
|
201
|
-
const reserved = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'LPT1', 'LPT2', 'LPT3'];
|
|
202
|
-
if (reserved.includes(filename.toUpperCase().split('.')[0])) {
|
|
203
|
-
return { valid: false, error: 'Filename is a reserved name' };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Check for hidden files starting with .
|
|
207
|
-
// (allowed but flagged)
|
|
208
|
-
const isHidden = filename.startsWith('.');
|
|
209
|
-
|
|
210
|
-
if (!PATTERNS.SAFE_FILENAME.test(filename)) {
|
|
211
|
-
const sanitized = filename.replace(/[:*?"<>|]/g, '_');
|
|
212
|
-
return { valid: false, error: 'Filename contains unsafe characters', sanitized };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return { valid: true, sanitized: filename, isHidden };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Validate and canonicalize a file path
|
|
220
|
-
* Prevents path traversal attacks by ensuring the resolved path
|
|
221
|
-
* is within the allowed base directory.
|
|
222
|
-
*
|
|
223
|
-
* @param {string} inputPath - User-provided path
|
|
224
|
-
* @param {string} baseDir - Base directory that path must be within
|
|
225
|
-
* @param {object} [options] - Options
|
|
226
|
-
* @param {boolean} [options.mustExist] - Path must exist
|
|
227
|
-
* @param {boolean} [options.allowSymlinks] - Allow symbolic links
|
|
228
|
-
* @returns {{ valid: boolean, error?: string, resolved?: string }}
|
|
229
|
-
*/
|
|
230
|
-
function validatePath(inputPath, baseDir, options = {}) {
|
|
231
|
-
const { mustExist = false, allowSymlinks = false } = options;
|
|
232
|
-
|
|
233
|
-
if (typeof inputPath !== 'string') {
|
|
234
|
-
return { valid: false, error: 'Path must be a string' };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (inputPath.length === 0) {
|
|
238
|
-
return { valid: false, error: 'Path cannot be empty' };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (inputPath.length > LIMITS.MAX_PATH_LENGTH) {
|
|
242
|
-
return { valid: false, error: `Path exceeds maximum length of ${LIMITS.MAX_PATH_LENGTH} characters` };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Check for null bytes (common path injection technique)
|
|
246
|
-
if (inputPath.includes('\x00')) {
|
|
247
|
-
return { valid: false, error: 'Path contains null bytes' };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Resolve and normalize the path
|
|
251
|
-
const resolvedBase = path.resolve(baseDir);
|
|
252
|
-
const resolvedPath = path.resolve(baseDir, inputPath);
|
|
253
|
-
|
|
254
|
-
// Check that resolved path is within base directory
|
|
255
|
-
const relative = path.relative(resolvedBase, resolvedPath);
|
|
256
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
257
|
-
return { valid: false, error: 'Path traversal detected: path escapes base directory' };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Check for symlinks if not allowed
|
|
261
|
-
if (!allowSymlinks && mustExist) {
|
|
262
|
-
try {
|
|
263
|
-
const stats = fs.lstatSync(resolvedPath);
|
|
264
|
-
if (stats.isSymbolicLink()) {
|
|
265
|
-
// Verify symlink target is also within bounds
|
|
266
|
-
const realPath = fs.realpathSync(resolvedPath);
|
|
267
|
-
const realRelative = path.relative(resolvedBase, realPath);
|
|
268
|
-
if (realRelative.startsWith('..') || path.isAbsolute(realRelative)) {
|
|
269
|
-
return { valid: false, error: 'Symlink target escapes base directory' };
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
} catch {
|
|
273
|
-
// Path doesn't exist, handled by mustExist check below
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Check existence if required
|
|
278
|
-
if (mustExist && !fs.existsSync(resolvedPath)) {
|
|
279
|
-
return { valid: false, error: 'Path does not exist' };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return { valid: true, resolved: resolvedPath };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Validate command arguments for safe shell execution
|
|
287
|
-
* @param {string[]} args - Arguments to validate
|
|
288
|
-
* @returns {{ valid: boolean, error?: string, sanitized?: string[] }}
|
|
289
|
-
*/
|
|
290
|
-
function validateCommandArgs(args) {
|
|
291
|
-
if (!Array.isArray(args)) {
|
|
292
|
-
return { valid: false, error: 'Arguments must be an array' };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (args.length > LIMITS.MAX_ARRAY_LENGTH) {
|
|
296
|
-
return { valid: false, error: `Too many arguments (max: ${LIMITS.MAX_ARRAY_LENGTH})` };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const sanitized = [];
|
|
300
|
-
const warnings = [];
|
|
301
|
-
|
|
302
|
-
for (let i = 0; i < args.length; i++) {
|
|
303
|
-
const arg = args[i];
|
|
304
|
-
|
|
305
|
-
if (typeof arg !== 'string') {
|
|
306
|
-
return { valid: false, error: `Argument at index ${i} must be a string` };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Check for dangerous shell characters
|
|
310
|
-
if (SHELL_DANGEROUS_CHARS.test(arg)) {
|
|
311
|
-
warnings.push(`Argument at index ${i} contains shell metacharacters`);
|
|
312
|
-
// Don't reject, but warn - spawn() is safe, but log for audit
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Check length
|
|
316
|
-
if (arg.length > LIMITS.MAX_STRING_LENGTH) {
|
|
317
|
-
return { valid: false, error: `Argument at index ${i} exceeds maximum length` };
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
sanitized.push(arg);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return { valid: true, sanitized, warnings: warnings.length > 0 ? warnings : undefined };
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Validate todo text
|
|
328
|
-
* @param {string} text - Todo text to validate
|
|
329
|
-
* @returns {{ valid: boolean, error?: string, sanitized?: string }}
|
|
330
|
-
*/
|
|
331
|
-
function validateTodoText(text) {
|
|
332
|
-
const result = validateStringLength(text, LIMITS.MAX_TODO_LENGTH, 1);
|
|
333
|
-
if (!result.valid) {
|
|
334
|
-
return result;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Remove control characters except newlines
|
|
338
|
-
const sanitized = stripUnsafeControlChars(text, { allowNewlines: true });
|
|
339
|
-
|
|
340
|
-
return { valid: true, sanitized };
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Validate description text
|
|
345
|
-
* @param {string} text - Description to validate
|
|
346
|
-
* @returns {{ valid: boolean, error?: string, sanitized?: string }}
|
|
347
|
-
*/
|
|
348
|
-
function validateDescription(text) {
|
|
349
|
-
const result = validateStringLength(text, LIMITS.MAX_DESCRIPTION_LENGTH, 0);
|
|
350
|
-
if (!result.valid) {
|
|
351
|
-
return result;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Remove control characters except newlines and tabs
|
|
355
|
-
const sanitized = stripUnsafeControlChars(text, { allowTabs: true, allowNewlines: true });
|
|
356
|
-
|
|
357
|
-
return { valid: true, sanitized };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Validate slug format
|
|
362
|
-
* @param {string} slug - Slug to validate
|
|
363
|
-
* @returns {{ valid: boolean, error?: string, sanitized?: string }}
|
|
364
|
-
*/
|
|
365
|
-
function validateSlug(slug) {
|
|
366
|
-
if (typeof slug !== 'string') {
|
|
367
|
-
return { valid: false, error: 'Slug must be a string' };
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const lengthResult = validateStringLength(slug, LIMITS.MAX_FILENAME_LENGTH, 1);
|
|
371
|
-
if (!lengthResult.valid) {
|
|
372
|
-
return lengthResult;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (!PATTERNS.SLUG.test(slug)) {
|
|
376
|
-
const sanitized = slug
|
|
377
|
-
.toLowerCase()
|
|
378
|
-
.replace(/[^a-z0-9-]/g, '-')
|
|
379
|
-
.replace(/^[^a-z]+/, '')
|
|
380
|
-
.replace(/-+/g, '-')
|
|
381
|
-
.replace(/-$/, '');
|
|
382
|
-
|
|
383
|
-
if (sanitized.length === 0 || !PATTERNS.SLUG.test(sanitized)) {
|
|
384
|
-
return { valid: false, error: 'Invalid slug format' };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
return { valid: false, error: 'Slug contains invalid characters', sanitized };
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return { valid: true, sanitized: slug };
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Validate email format
|
|
395
|
-
* @param {string} email - Email to validate
|
|
396
|
-
* @returns {{ valid: boolean, error?: string }}
|
|
397
|
-
*/
|
|
398
|
-
function validateEmail(email) {
|
|
399
|
-
if (typeof email !== 'string') {
|
|
400
|
-
return { valid: false, error: 'Email must be a string' };
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (email.length > 254) {
|
|
404
|
-
return { valid: false, error: 'Email exceeds maximum length' };
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (!PATTERNS.EMAIL.test(email)) {
|
|
408
|
-
return { valid: false, error: 'Invalid email format' };
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return { valid: true };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Validate URL format
|
|
416
|
-
* @param {string} url - URL to validate
|
|
417
|
-
* @returns {{ valid: boolean, error?: string }}
|
|
418
|
-
*/
|
|
419
|
-
function validateUrl(url) {
|
|
420
|
-
if (typeof url !== 'string') {
|
|
421
|
-
return { valid: false, error: 'URL must be a string' };
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (url.length > LIMITS.MAX_STRING_LENGTH) {
|
|
425
|
-
return { valid: false, error: 'URL exceeds maximum length' };
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (!PATTERNS.URL.test(url)) {
|
|
429
|
-
return { valid: false, error: 'Invalid URL format' };
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Additional security check - no credentials in URL
|
|
433
|
-
try {
|
|
434
|
-
const parsed = new URL(url);
|
|
435
|
-
if (parsed.username || parsed.password) {
|
|
436
|
-
return { valid: false, error: 'URL cannot contain embedded credentials' };
|
|
437
|
-
}
|
|
438
|
-
} catch {
|
|
439
|
-
return { valid: false, error: 'Invalid URL format' };
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return { valid: true };
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Validate array input
|
|
447
|
-
* @param {Array} arr - Array to validate
|
|
448
|
-
* @param {Function} [itemValidator] - Optional validator for each item
|
|
449
|
-
* @param {object} [options] - Options
|
|
450
|
-
* @param {number} [options.maxLength] - Maximum array length
|
|
451
|
-
* @param {number} [options.minLength] - Minimum array length
|
|
452
|
-
* @returns {{ valid: boolean, error?: string, sanitized?: Array }}
|
|
453
|
-
*/
|
|
454
|
-
function validateArray(arr, itemValidator, options = {}) {
|
|
455
|
-
const { maxLength = LIMITS.MAX_ARRAY_LENGTH, minLength = 0 } = options;
|
|
456
|
-
|
|
457
|
-
if (!Array.isArray(arr)) {
|
|
458
|
-
return { valid: false, error: 'Value must be an array' };
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (arr.length < minLength) {
|
|
462
|
-
return { valid: false, error: `Array must have at least ${minLength} items` };
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if (arr.length > maxLength) {
|
|
466
|
-
return { valid: false, error: `Array exceeds maximum length of ${maxLength} items` };
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (!itemValidator) {
|
|
470
|
-
return { valid: true, sanitized: arr };
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const sanitized = [];
|
|
474
|
-
for (let i = 0; i < arr.length; i++) {
|
|
475
|
-
const result = itemValidator(arr[i]);
|
|
476
|
-
if (!result.valid) {
|
|
477
|
-
return { valid: false, error: `Item at index ${i}: ${result.error}` };
|
|
478
|
-
}
|
|
479
|
-
sanitized.push(result.sanitized !== undefined ? result.sanitized : arr[i]);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
return { valid: true, sanitized };
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Create a validation chain for multiple validators
|
|
487
|
-
* @param {Array<Function>} validators - Validators to run in sequence
|
|
488
|
-
* @returns {Function} Combined validator
|
|
489
|
-
*/
|
|
490
|
-
function chain(...validators) {
|
|
491
|
-
return (value) => {
|
|
492
|
-
let current = value;
|
|
493
|
-
for (const validator of validators) {
|
|
494
|
-
const result = validator(current);
|
|
495
|
-
if (!result.valid) {
|
|
496
|
-
return result;
|
|
497
|
-
}
|
|
498
|
-
if (result.sanitized !== undefined) {
|
|
499
|
-
current = result.sanitized;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
return { valid: true, sanitized: current };
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Validate and sanitize user input with a schema
|
|
508
|
-
* @param {object} input - Input object to validate
|
|
509
|
-
* @param {object} schema - Validation schema
|
|
510
|
-
* @returns {{ valid: boolean, errors?: string[], sanitized?: object }}
|
|
511
|
-
*/
|
|
512
|
-
function validateInput(input, schema) {
|
|
513
|
-
if (typeof input !== 'object' || input === null) {
|
|
514
|
-
return { valid: false, errors: ['Input must be an object'] };
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
const errors = [];
|
|
518
|
-
const sanitized = {};
|
|
519
|
-
|
|
520
|
-
for (const [key, config] of Object.entries(schema)) {
|
|
521
|
-
const value = input[key];
|
|
522
|
-
|
|
523
|
-
// Check required
|
|
524
|
-
if (config.required && (value === undefined || value === null || value === '')) {
|
|
525
|
-
errors.push(`${key} is required`);
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Skip optional missing values
|
|
530
|
-
if (value === undefined || value === null) {
|
|
531
|
-
if (config.default !== undefined) {
|
|
532
|
-
sanitized[key] = config.default;
|
|
533
|
-
}
|
|
534
|
-
continue;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// Run validator
|
|
538
|
-
if (config.validator) {
|
|
539
|
-
const result = config.validator(value);
|
|
540
|
-
if (!result.valid) {
|
|
541
|
-
errors.push(`${key}: ${result.error}`);
|
|
542
|
-
} else {
|
|
543
|
-
sanitized[key] = result.sanitized !== undefined ? result.sanitized : value;
|
|
544
|
-
}
|
|
545
|
-
} else {
|
|
546
|
-
sanitized[key] = value;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return errors.length > 0
|
|
551
|
-
? { valid: false, errors }
|
|
552
|
-
: { valid: true, sanitized };
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
module.exports = {
|
|
556
|
-
LIMITS,
|
|
557
|
-
PATTERNS,
|
|
558
|
-
validateStringLength,
|
|
559
|
-
validateNumericId,
|
|
560
|
-
validateProjectName,
|
|
561
|
-
validateFilename,
|
|
562
|
-
validatePath,
|
|
563
|
-
validateCommandArgs,
|
|
564
|
-
validateTodoText,
|
|
565
|
-
validateDescription,
|
|
566
|
-
validateSlug,
|
|
567
|
-
validateEmail,
|
|
568
|
-
validateUrl,
|
|
569
|
-
validateArray,
|
|
570
|
-
validateInput,
|
|
571
|
-
chain
|
|
572
|
-
};
|
package/mcp/server.d.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bootspring MCP Server Types
|
|
3
|
-
* @module mcp
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface ToolDefinition {
|
|
7
|
-
name: string;
|
|
8
|
-
description: string;
|
|
9
|
-
inputSchema: {
|
|
10
|
-
type: 'object';
|
|
11
|
-
properties: Record<string, unknown>;
|
|
12
|
-
required?: string[];
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ResourceDefinition {
|
|
17
|
-
uri: string;
|
|
18
|
-
name: string;
|
|
19
|
-
description: string;
|
|
20
|
-
mimeType: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ToolResult {
|
|
24
|
-
content: Array<{
|
|
25
|
-
type: 'text';
|
|
26
|
-
text: string;
|
|
27
|
-
}>;
|
|
28
|
-
isError?: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export type ToolHandler = (args: Record<string, unknown>) => Promise<ToolResult>;
|
|
32
|
-
export type ResourceHandler = () => Promise<{ contents: Array<{ uri: string; text: string; mimeType: string }> }>;
|
|
33
|
-
|
|
34
|
-
/** Available MCP tools */
|
|
35
|
-
export const TOOLS: ToolDefinition[];
|
|
36
|
-
|
|
37
|
-
/** Available MCP resources */
|
|
38
|
-
export const RESOURCES: ResourceDefinition[];
|
|
39
|
-
|
|
40
|
-
/** Tool handlers by name */
|
|
41
|
-
export const toolHandlers: Record<string, ToolHandler>;
|
|
42
|
-
|
|
43
|
-
/** Resource handlers by URI */
|
|
44
|
-
export const resourceHandlers: Record<string, ResourceHandler>;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Track telemetry event
|
|
48
|
-
* @param event - Event name
|
|
49
|
-
* @param data - Event data
|
|
50
|
-
*/
|
|
51
|
-
export function trackTelemetry(event: string, data?: Record<string, unknown>): void;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Start the MCP server
|
|
55
|
-
* Connects to stdio transport and handles tool/resource requests
|
|
56
|
-
*/
|
|
57
|
-
export function main(): Promise<void>;
|