@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.
Files changed (45) hide show
  1. package/bin/bootspring.js +127 -73
  2. package/claude-commands/agent.md +34 -0
  3. package/claude-commands/bs.md +31 -0
  4. package/claude-commands/build.md +25 -0
  5. package/claude-commands/skill.md +31 -0
  6. package/claude-commands/todo.md +25 -0
  7. package/dist/core/index.d.ts +5814 -0
  8. package/dist/core.js +5779 -0
  9. package/dist/index.js +93883 -0
  10. package/dist/mcp/index.d.ts +1 -0
  11. package/dist/mcp-server.js +2298 -0
  12. package/package.json +22 -55
  13. package/core/api-client.d.ts +0 -69
  14. package/core/api-client.js +0 -1482
  15. package/core/auth.d.ts +0 -98
  16. package/core/auth.js +0 -737
  17. package/core/build-orchestrator.js +0 -508
  18. package/core/build-state.js +0 -612
  19. package/core/config.d.ts +0 -106
  20. package/core/config.js +0 -1328
  21. package/core/context-loader.js +0 -580
  22. package/core/context.d.ts +0 -61
  23. package/core/context.js +0 -327
  24. package/core/entitlements.d.ts +0 -70
  25. package/core/entitlements.js +0 -322
  26. package/core/index.d.ts +0 -53
  27. package/core/index.js +0 -62
  28. package/core/mcp-config.js +0 -115
  29. package/core/policies.d.ts +0 -43
  30. package/core/policies.js +0 -113
  31. package/core/policy-matrix.js +0 -303
  32. package/core/project-activity.js +0 -175
  33. package/core/redaction.d.ts +0 -5
  34. package/core/redaction.js +0 -63
  35. package/core/self-update.js +0 -259
  36. package/core/session.js +0 -353
  37. package/core/task-extractor.js +0 -1098
  38. package/core/telemetry.d.ts +0 -55
  39. package/core/telemetry.js +0 -617
  40. package/core/tier-enforcement.js +0 -928
  41. package/core/utils.d.ts +0 -90
  42. package/core/utils.js +0 -455
  43. package/core/validation.js +0 -572
  44. package/mcp/server.d.ts +0 -57
  45. package/mcp/server.js +0 -264
@@ -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>;