@fractary/faber-cli 1.0.0 → 1.2.0
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/dist/commands/init.js +1 -1
- package/dist/commands/plan/index.d.ts +11 -0
- package/dist/commands/plan/index.d.ts.map +1 -0
- package/dist/commands/plan/index.js +383 -0
- package/dist/commands/workflow/index.d.ts +7 -7
- package/dist/commands/workflow/index.js +17 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +90 -8
- package/dist/lib/anthropic-client.d.ts +69 -0
- package/dist/lib/anthropic-client.d.ts.map +1 -0
- package/dist/lib/anthropic-client.js +225 -0
- package/dist/lib/config.d.ts +30 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +127 -0
- package/dist/lib/repo-client.d.ts +81 -0
- package/dist/lib/repo-client.d.ts.map +1 -0
- package/dist/lib/repo-client.js +105 -0
- package/dist/types/config.d.ts +34 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +6 -0
- package/dist/utils/prompt.d.ts +8 -0
- package/dist/utils/prompt.d.ts.map +1 -0
- package/dist/utils/prompt.js +19 -0
- package/dist/utils/validation.d.ts +71 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +157 -0
- package/package.json +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo Client
|
|
3
|
+
*
|
|
4
|
+
* Wrapper for fractary-repo plugin commands (CLI-based)
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Repo Client - wraps fractary-repo plugin CLI operations
|
|
8
|
+
*
|
|
9
|
+
* Note: This calls fractary-repo CLI commands as specified in SPEC-00030.
|
|
10
|
+
* These commands must be implemented in the fractary-repo plugin.
|
|
11
|
+
*/
|
|
12
|
+
export class RepoClient {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
const token = config.github?.token;
|
|
16
|
+
if (!token) {
|
|
17
|
+
throw new Error('GitHub token not found. Set GITHUB_TOKEN environment variable.');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Fetch specific issues by ID
|
|
22
|
+
*
|
|
23
|
+
* Calls: fractary-repo issue-fetch --ids 258,259,260 --format json
|
|
24
|
+
*/
|
|
25
|
+
async fetchIssues(ids) {
|
|
26
|
+
// TODO: Call fractary-repo CLI when available
|
|
27
|
+
// const result = await this.callRepoCommand('issue-fetch', ['--ids', ids.join(','), '--format', 'json']);
|
|
28
|
+
// Placeholder implementation
|
|
29
|
+
throw new Error('fractary-repo issue-fetch command not yet implemented. See SPEC-00030.');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Search issues by labels
|
|
33
|
+
*
|
|
34
|
+
* Calls: fractary-repo issue-search --labels "workflow:etl,status:approved" --format json
|
|
35
|
+
*/
|
|
36
|
+
async searchIssues(labels) {
|
|
37
|
+
// TODO: Call fractary-repo CLI when available
|
|
38
|
+
// const result = await this.callRepoCommand('issue-search', ['--labels', labels.join(','), '--format', 'json']);
|
|
39
|
+
// Placeholder implementation
|
|
40
|
+
throw new Error('fractary-repo issue-search command not yet implemented. See SPEC-00030.');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create a git branch
|
|
44
|
+
*
|
|
45
|
+
* Calls: fractary-repo branch-create <branch-name> --format json
|
|
46
|
+
*/
|
|
47
|
+
async createBranch(branchName) {
|
|
48
|
+
// TODO: Call fractary-repo CLI when available
|
|
49
|
+
// await this.callRepoCommand('branch-create', [branchName, '--format', 'json']);
|
|
50
|
+
// Placeholder implementation
|
|
51
|
+
throw new Error('fractary-repo branch-create command not yet implemented. See SPEC-00030.');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create a git worktree
|
|
55
|
+
*
|
|
56
|
+
* Calls: fractary-repo worktree-create --work-id 258 --format json
|
|
57
|
+
*/
|
|
58
|
+
async createWorktree(options) {
|
|
59
|
+
// TODO: Call fractary-repo CLI when available
|
|
60
|
+
// const args = ['--work-id', options.workId, '--format', 'json'];
|
|
61
|
+
// if (options.path) {
|
|
62
|
+
// args.push('--path', options.path);
|
|
63
|
+
// }
|
|
64
|
+
// const result = await this.callRepoCommand('worktree-create', args);
|
|
65
|
+
// return result;
|
|
66
|
+
// Placeholder implementation
|
|
67
|
+
throw new Error('fractary-repo worktree-create command not yet implemented. See SPEC-00030.');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Update GitHub issue
|
|
71
|
+
*
|
|
72
|
+
* Calls: fractary-repo issue-update --id 258 --comment "..." --add-label "..."
|
|
73
|
+
*/
|
|
74
|
+
async updateIssue(options) {
|
|
75
|
+
// TODO: Call fractary-repo CLI when available
|
|
76
|
+
// const args = ['--id', options.id];
|
|
77
|
+
// if (options.comment) {
|
|
78
|
+
// args.push('--comment', options.comment);
|
|
79
|
+
// }
|
|
80
|
+
// if (options.addLabel) {
|
|
81
|
+
// args.push('--add-label', options.addLabel);
|
|
82
|
+
// }
|
|
83
|
+
// if (options.removeLabel) {
|
|
84
|
+
// args.push('--remove-label', options.removeLabel);
|
|
85
|
+
// }
|
|
86
|
+
// await this.callRepoCommand('issue-update', args);
|
|
87
|
+
// Placeholder implementation
|
|
88
|
+
throw new Error('fractary-repo issue-update command not yet implemented. See SPEC-00030.');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Call fractary-repo CLI command
|
|
92
|
+
*
|
|
93
|
+
* This will be implemented to spawn fractary-repo CLI process safely.
|
|
94
|
+
* For now, this is a placeholder showing the intended interface.
|
|
95
|
+
*/
|
|
96
|
+
async callRepoCommand(command, args) {
|
|
97
|
+
// Implementation will use safe process spawning to call:
|
|
98
|
+
// fractary-repo <command> [args...]
|
|
99
|
+
//
|
|
100
|
+
// Example: fractary-repo issue-fetch --ids 258,259 --format json
|
|
101
|
+
//
|
|
102
|
+
// The command will return JSON which we parse and return.
|
|
103
|
+
throw new Error(`fractary-repo ${command} not yet implemented`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FABER CLI Configuration Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for configuration objects
|
|
5
|
+
*/
|
|
6
|
+
export interface AnthropicConfig {
|
|
7
|
+
api_key?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface GitHubConfig {
|
|
10
|
+
token?: string;
|
|
11
|
+
organization?: string;
|
|
12
|
+
project?: string;
|
|
13
|
+
repo?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface WorktreeConfig {
|
|
16
|
+
location?: string;
|
|
17
|
+
inherit_from_claude?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface WorkflowConfig {
|
|
20
|
+
default?: string;
|
|
21
|
+
config_path?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface FaberConfig {
|
|
24
|
+
anthropic?: AnthropicConfig;
|
|
25
|
+
github?: GitHubConfig;
|
|
26
|
+
worktree?: WorktreeConfig;
|
|
27
|
+
workflow?: WorkflowConfig;
|
|
28
|
+
}
|
|
29
|
+
export interface ClaudeConfig {
|
|
30
|
+
worktree?: {
|
|
31
|
+
directory?: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE;QACT,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;CACH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/utils/prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;GAEG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYxD"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple prompt utility for CLI input
|
|
3
|
+
*/
|
|
4
|
+
import * as readline from 'readline';
|
|
5
|
+
/**
|
|
6
|
+
* Prompt user for input
|
|
7
|
+
*/
|
|
8
|
+
export function prompt(question) {
|
|
9
|
+
const rl = readline.createInterface({
|
|
10
|
+
input: process.stdin,
|
|
11
|
+
output: process.stdout,
|
|
12
|
+
});
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve(answer.trim());
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -27,4 +27,75 @@ export declare function parseOptionalInteger(value: string | undefined, fieldNam
|
|
|
27
27
|
* @throws Error if value is not a valid positive integer
|
|
28
28
|
*/
|
|
29
29
|
export declare function parsePositiveInteger(value: string, fieldName: string): number;
|
|
30
|
+
/**
|
|
31
|
+
* Validates work ID format
|
|
32
|
+
* Work IDs must be numeric (GitHub issue numbers)
|
|
33
|
+
*
|
|
34
|
+
* @param workId - Work ID to validate
|
|
35
|
+
* @returns True if valid
|
|
36
|
+
* @throws Error if invalid
|
|
37
|
+
*/
|
|
38
|
+
export declare function validateWorkId(workId: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Validates multiple comma-separated work IDs
|
|
41
|
+
*
|
|
42
|
+
* @param workIds - Comma-separated work IDs
|
|
43
|
+
* @returns Array of validated work IDs
|
|
44
|
+
* @throws Error if any ID is invalid
|
|
45
|
+
*/
|
|
46
|
+
export declare function validateWorkIds(workIds: string): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Validates label format
|
|
49
|
+
* Labels must be alphanumeric with hyphens, colons, and underscores only
|
|
50
|
+
*
|
|
51
|
+
* @param label - Label to validate
|
|
52
|
+
* @returns True if valid
|
|
53
|
+
* @throws Error if invalid
|
|
54
|
+
*/
|
|
55
|
+
export declare function validateLabel(label: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Validates multiple comma-separated labels
|
|
58
|
+
*
|
|
59
|
+
* @param labels - Comma-separated labels
|
|
60
|
+
* @returns Array of validated labels
|
|
61
|
+
* @throws Error if any label is invalid
|
|
62
|
+
*/
|
|
63
|
+
export declare function validateLabels(labels: string): string[];
|
|
64
|
+
/**
|
|
65
|
+
* Validates workflow name format
|
|
66
|
+
* Workflow names must be alphanumeric with hyphens and underscores
|
|
67
|
+
*
|
|
68
|
+
* @param workflowName - Workflow name to validate
|
|
69
|
+
* @returns True if valid
|
|
70
|
+
* @throws Error if invalid
|
|
71
|
+
*/
|
|
72
|
+
export declare function validateWorkflowName(workflowName: string): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Validates and sanitizes file path to prevent path traversal
|
|
75
|
+
* Ensures path doesn't contain dangerous patterns like ../, absolute paths, etc.
|
|
76
|
+
*
|
|
77
|
+
* @param filePath - File path to validate
|
|
78
|
+
* @param baseDir - Base directory that path must be relative to (optional)
|
|
79
|
+
* @returns Sanitized path
|
|
80
|
+
* @throws Error if path is unsafe
|
|
81
|
+
*/
|
|
82
|
+
export declare function validateSafePath(filePath: string, baseDir?: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* Validates JSON response size to prevent DoS attacks
|
|
85
|
+
*
|
|
86
|
+
* @param jsonString - JSON string to validate
|
|
87
|
+
* @param maxSizeBytes - Maximum allowed size in bytes (default: 1MB)
|
|
88
|
+
* @returns True if valid
|
|
89
|
+
* @throws Error if too large
|
|
90
|
+
*/
|
|
91
|
+
export declare function validateJsonSize(jsonString: string, maxSizeBytes?: number): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Validates plan ID format
|
|
94
|
+
* Plan IDs follow format: fractary-faber-{work-id}-{YYYYMMDD}-{HHMMSS}
|
|
95
|
+
*
|
|
96
|
+
* @param planId - Plan ID to validate
|
|
97
|
+
* @returns True if valid
|
|
98
|
+
* @throws Error if invalid
|
|
99
|
+
*/
|
|
100
|
+
export declare function validatePlanId(planId: string): boolean;
|
|
30
101
|
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAY1E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrG;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQ7E"}
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAY1E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrG;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQ7E;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAWtD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBzD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAYpD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBvD;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAYlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAoC3E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,GAAE,MAAoB,GAAG,OAAO,CAYhG;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAUtD"}
|
package/dist/utils/validation.js
CHANGED
|
@@ -47,3 +47,160 @@ export function parsePositiveInteger(value, fieldName) {
|
|
|
47
47
|
}
|
|
48
48
|
return parsed;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Validates work ID format
|
|
52
|
+
* Work IDs must be numeric (GitHub issue numbers)
|
|
53
|
+
*
|
|
54
|
+
* @param workId - Work ID to validate
|
|
55
|
+
* @returns True if valid
|
|
56
|
+
* @throws Error if invalid
|
|
57
|
+
*/
|
|
58
|
+
export function validateWorkId(workId) {
|
|
59
|
+
// Work IDs must be numeric (1-8 digits for GitHub issue numbers)
|
|
60
|
+
const workIdPattern = /^\d{1,8}$/;
|
|
61
|
+
if (!workIdPattern.test(workId)) {
|
|
62
|
+
throw new Error(`Invalid work ID format: "${workId}". Work IDs must be numeric (e.g., "123", "456").`);
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validates multiple comma-separated work IDs
|
|
68
|
+
*
|
|
69
|
+
* @param workIds - Comma-separated work IDs
|
|
70
|
+
* @returns Array of validated work IDs
|
|
71
|
+
* @throws Error if any ID is invalid
|
|
72
|
+
*/
|
|
73
|
+
export function validateWorkIds(workIds) {
|
|
74
|
+
const ids = workIds.split(',').map(id => id.trim()).filter(Boolean);
|
|
75
|
+
if (ids.length === 0) {
|
|
76
|
+
throw new Error('No work IDs provided');
|
|
77
|
+
}
|
|
78
|
+
if (ids.length > 50) {
|
|
79
|
+
throw new Error(`Too many work IDs (${ids.length}). Maximum is 50 to prevent API overload.`);
|
|
80
|
+
}
|
|
81
|
+
// Validate each ID
|
|
82
|
+
ids.forEach(id => validateWorkId(id));
|
|
83
|
+
return ids;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Validates label format
|
|
87
|
+
* Labels must be alphanumeric with hyphens, colons, and underscores only
|
|
88
|
+
*
|
|
89
|
+
* @param label - Label to validate
|
|
90
|
+
* @returns True if valid
|
|
91
|
+
* @throws Error if invalid
|
|
92
|
+
*/
|
|
93
|
+
export function validateLabel(label) {
|
|
94
|
+
// Labels: alphanumeric, hyphens, colons, underscores (GitHub label format)
|
|
95
|
+
// Examples: "workflow:etl", "status:approved", "priority-high"
|
|
96
|
+
const labelPattern = /^[a-zA-Z0-9_:-]{1,50}$/;
|
|
97
|
+
if (!labelPattern.test(label)) {
|
|
98
|
+
throw new Error(`Invalid label format: "${label}". Labels must be alphanumeric with hyphens, colons, or underscores (max 50 chars).`);
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Validates multiple comma-separated labels
|
|
104
|
+
*
|
|
105
|
+
* @param labels - Comma-separated labels
|
|
106
|
+
* @returns Array of validated labels
|
|
107
|
+
* @throws Error if any label is invalid
|
|
108
|
+
*/
|
|
109
|
+
export function validateLabels(labels) {
|
|
110
|
+
const labelArray = labels.split(',').map(l => l.trim()).filter(Boolean);
|
|
111
|
+
if (labelArray.length === 0) {
|
|
112
|
+
throw new Error('No labels provided');
|
|
113
|
+
}
|
|
114
|
+
if (labelArray.length > 20) {
|
|
115
|
+
throw new Error(`Too many labels (${labelArray.length}). Maximum is 20.`);
|
|
116
|
+
}
|
|
117
|
+
// Validate each label
|
|
118
|
+
labelArray.forEach(label => validateLabel(label));
|
|
119
|
+
return labelArray;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Validates workflow name format
|
|
123
|
+
* Workflow names must be alphanumeric with hyphens and underscores
|
|
124
|
+
*
|
|
125
|
+
* @param workflowName - Workflow name to validate
|
|
126
|
+
* @returns True if valid
|
|
127
|
+
* @throws Error if invalid
|
|
128
|
+
*/
|
|
129
|
+
export function validateWorkflowName(workflowName) {
|
|
130
|
+
// Workflow names: alphanumeric, hyphens, underscores
|
|
131
|
+
// Examples: "etl", "data-pipeline", "bugfix"
|
|
132
|
+
const workflowPattern = /^[a-zA-Z0-9_-]{1,50}$/;
|
|
133
|
+
if (!workflowPattern.test(workflowName)) {
|
|
134
|
+
throw new Error(`Invalid workflow name: "${workflowName}". Workflow names must be alphanumeric with hyphens or underscores (max 50 chars).`);
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Validates and sanitizes file path to prevent path traversal
|
|
140
|
+
* Ensures path doesn't contain dangerous patterns like ../, absolute paths, etc.
|
|
141
|
+
*
|
|
142
|
+
* @param filePath - File path to validate
|
|
143
|
+
* @param baseDir - Base directory that path must be relative to (optional)
|
|
144
|
+
* @returns Sanitized path
|
|
145
|
+
* @throws Error if path is unsafe
|
|
146
|
+
*/
|
|
147
|
+
export function validateSafePath(filePath, baseDir) {
|
|
148
|
+
// Remove any null bytes
|
|
149
|
+
if (filePath.includes('\0')) {
|
|
150
|
+
throw new Error('Path contains null bytes');
|
|
151
|
+
}
|
|
152
|
+
// Check for path traversal patterns
|
|
153
|
+
const dangerousPatterns = [
|
|
154
|
+
/\.\./, // Parent directory (..)
|
|
155
|
+
/^\//, // Absolute path
|
|
156
|
+
/^[a-zA-Z]:\\/, // Windows absolute path
|
|
157
|
+
/^~\//, // Home directory
|
|
158
|
+
];
|
|
159
|
+
for (const pattern of dangerousPatterns) {
|
|
160
|
+
if (pattern.test(filePath)) {
|
|
161
|
+
throw new Error(`Unsafe path detected: "${filePath}". Paths must be relative and cannot contain "..", absolute paths, or home directory references.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Normalize path (remove redundant separators, etc.)
|
|
165
|
+
const normalized = filePath.replace(/\/+/g, '/').replace(/\\/g, '/');
|
|
166
|
+
// If base directory provided, ensure path starts with it
|
|
167
|
+
if (baseDir) {
|
|
168
|
+
const normalizedBase = baseDir.replace(/\/+$/, '');
|
|
169
|
+
if (!normalized.startsWith(normalizedBase)) {
|
|
170
|
+
throw new Error(`Path "${filePath}" must be within base directory "${baseDir}"`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return normalized;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Validates JSON response size to prevent DoS attacks
|
|
177
|
+
*
|
|
178
|
+
* @param jsonString - JSON string to validate
|
|
179
|
+
* @param maxSizeBytes - Maximum allowed size in bytes (default: 1MB)
|
|
180
|
+
* @returns True if valid
|
|
181
|
+
* @throws Error if too large
|
|
182
|
+
*/
|
|
183
|
+
export function validateJsonSize(jsonString, maxSizeBytes = 1024 * 1024) {
|
|
184
|
+
const sizeBytes = Buffer.byteLength(jsonString, 'utf8');
|
|
185
|
+
if (sizeBytes > maxSizeBytes) {
|
|
186
|
+
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
|
|
187
|
+
const maxMB = (maxSizeBytes / 1024 / 1024).toFixed(2);
|
|
188
|
+
throw new Error(`JSON response too large: ${sizeMB}MB (maximum: ${maxMB}MB). This may indicate an API error or DoS attempt.`);
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Validates plan ID format
|
|
194
|
+
* Plan IDs follow format: fractary-faber-{work-id}-{YYYYMMDD}-{HHMMSS}
|
|
195
|
+
*
|
|
196
|
+
* @param planId - Plan ID to validate
|
|
197
|
+
* @returns True if valid
|
|
198
|
+
* @throws Error if invalid
|
|
199
|
+
*/
|
|
200
|
+
export function validatePlanId(planId) {
|
|
201
|
+
const planIdPattern = /^fractary-faber-\d+-\d{8}-\d{6}$/;
|
|
202
|
+
if (!planIdPattern.test(planId)) {
|
|
203
|
+
throw new Error(`Invalid plan ID format: "${planId}". Expected format: fractary-faber-{work-id}-{YYYYMMDD}-{HHMMSS}`);
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
}
|