@veestack-tools/cli 3.0.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/.env.example +14 -0
- package/bin/veestack.mjs +2 -0
- package/package.json +29 -0
- package/src/commands/login.ts +60 -0
- package/src/commands/scan.ts +219 -0
- package/src/commands/upload.ts +241 -0
- package/src/index.ts +36 -0
- package/src/wasm/license.ts +181 -0
- package/src/wasm/loader.ts +409 -0
- package/tsconfig.json +19 -0
- package/tsup.config.ts +11 -0
package/.env.example
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# VeeStack CLI Environment Variables
|
|
2
|
+
# Copy this file to .env and fill in your actual values
|
|
3
|
+
|
|
4
|
+
# Supabase Configuration (Required)
|
|
5
|
+
SUPABASE_URL=https://your-project.supabase.co
|
|
6
|
+
SUPABASE_ANON_KEY=your_anon_key_here
|
|
7
|
+
|
|
8
|
+
# IMPORTANT SECURITY NOTES:
|
|
9
|
+
# - CLI should use SUPABASE_ANON_KEY (not Service Role Key)
|
|
10
|
+
# - User authentication is handled via OAuth browser flow
|
|
11
|
+
# - Access token is stored securely after login
|
|
12
|
+
|
|
13
|
+
# Optional: For admin/debug scripts only (never commit this)
|
|
14
|
+
# SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_here
|
package/bin/veestack.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@veestack-tools/cli",
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"veestack": "bin/veestack.mjs"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsup",
|
|
10
|
+
"dev": "tsup --watch",
|
|
11
|
+
"start": "node bin/veestack.mjs"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@veestack-tools/types": "workspace:*",
|
|
15
|
+
"@veestack-tools/utils": "workspace:*",
|
|
16
|
+
"@veestack-tools/engine": "workspace:*",
|
|
17
|
+
"commander": "^11.1.0",
|
|
18
|
+
"chalk": "^5.3.0",
|
|
19
|
+
"ora": "^8.0.1",
|
|
20
|
+
"glob": "^10.3.10",
|
|
21
|
+
"prompts": "^2.4.2"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^20.11.5",
|
|
25
|
+
"@types/prompts": "^2.4.9",
|
|
26
|
+
"tsup": "^8.0.2",
|
|
27
|
+
"typescript": "^5.3.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import prompts from 'prompts';
|
|
6
|
+
|
|
7
|
+
export async function loginCommand(options: { key?: string }): Promise<void> {
|
|
8
|
+
const spinner = ora({
|
|
9
|
+
text: 'Authenticating...',
|
|
10
|
+
spinner: 'dots',
|
|
11
|
+
stream: process.stdout
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
let apiKey = options.key;
|
|
16
|
+
|
|
17
|
+
if (!apiKey) {
|
|
18
|
+
const response = await prompts({
|
|
19
|
+
type: 'password',
|
|
20
|
+
name: 'key',
|
|
21
|
+
message: 'Enter your VeeStack API key:',
|
|
22
|
+
validate: (value) => value.length > 0 || 'API key is required'
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!response.key) {
|
|
26
|
+
console.log('❌ Login cancelled');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
apiKey = response.key;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
spinner.start('Validating API key...');
|
|
34
|
+
|
|
35
|
+
// Mock validation - in production this would validate with the server
|
|
36
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
37
|
+
|
|
38
|
+
// Save to config file
|
|
39
|
+
const configDir = join(homedir(), '.veestack');
|
|
40
|
+
const configFile = join(configDir, 'config.json');
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const fs = await import('fs/promises');
|
|
44
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
45
|
+
await fs.writeFile(configFile, JSON.stringify({ apiKey }, null, 2));
|
|
46
|
+
} catch {
|
|
47
|
+
// Fallback to sync
|
|
48
|
+
const fs = await import('fs');
|
|
49
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
50
|
+
writeFileSync(configFile, JSON.stringify({ apiKey }, null, 2));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
spinner.succeed('Authenticated successfully');
|
|
54
|
+
console.log('✅ API key saved to ~/.veestack/config.json');
|
|
55
|
+
} catch (error) {
|
|
56
|
+
spinner.fail('Authentication failed');
|
|
57
|
+
console.error(error);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { glob } from 'glob';
|
|
2
|
+
import { readFileSync, statSync, readdirSync } from 'fs';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
|
+
import type { Snapshot, FileNode, DependencyNode } from '@veestack-tools/types';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
|
|
8
|
+
export async function scanCommand(options: {
|
|
9
|
+
path: string;
|
|
10
|
+
output: string;
|
|
11
|
+
}): Promise<void> {
|
|
12
|
+
const spinner = ora({
|
|
13
|
+
text: 'Scanning project...',
|
|
14
|
+
spinner: 'dots',
|
|
15
|
+
stream: process.stdout
|
|
16
|
+
}).start();
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const snapshot = await generateSnapshot(options.path);
|
|
20
|
+
|
|
21
|
+
spinner.succeed(`Scanned ${snapshot.metadata.total_files} files`);
|
|
22
|
+
|
|
23
|
+
// Save to file
|
|
24
|
+
const fs = await import('fs/promises');
|
|
25
|
+
await fs.writeFile(options.output, JSON.stringify(snapshot, null, 2));
|
|
26
|
+
|
|
27
|
+
console.log(`\n✅ Snapshot saved to ${options.output}`);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
spinner.fail('Scan failed');
|
|
30
|
+
console.error(error);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function generateSnapshot(projectPath: string): Promise<Snapshot> {
|
|
36
|
+
const files = await scanFiles(projectPath);
|
|
37
|
+
const dependencies = await scanDependencies(projectPath);
|
|
38
|
+
|
|
39
|
+
const metadata = {
|
|
40
|
+
total_files: files.length,
|
|
41
|
+
total_dependencies: dependencies.length,
|
|
42
|
+
total_size_bytes: files.reduce((sum, f) => sum + f.size_bytes, 0),
|
|
43
|
+
total_lines: files.reduce((sum, f) => sum + (f.estimated_lines || 0), 0),
|
|
44
|
+
max_directory_depth: Math.max(...files.map((f) => f.depth), 0),
|
|
45
|
+
language_breakdown: calculateLanguageBreakdown(files),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
snapshot_version: '1.0.0',
|
|
50
|
+
engine_target_version: '1.0.0',
|
|
51
|
+
project_id: crypto.randomUUID(),
|
|
52
|
+
generated_at: new Date().toISOString(),
|
|
53
|
+
root_path_hash: hashString(projectPath),
|
|
54
|
+
project_root: resolve(projectPath), // Use absolute path for content scanning
|
|
55
|
+
metadata,
|
|
56
|
+
files,
|
|
57
|
+
dependencies,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function scanFiles(projectPath: string): Promise<FileNode[]> {
|
|
62
|
+
const files: FileNode[] = [];
|
|
63
|
+
const patterns = ['**/*'];
|
|
64
|
+
|
|
65
|
+
const filePaths = await glob(patterns, {
|
|
66
|
+
cwd: projectPath,
|
|
67
|
+
ignore: [
|
|
68
|
+
'**/node_modules/**',
|
|
69
|
+
'**/.git/**',
|
|
70
|
+
'**/dist/**',
|
|
71
|
+
'**/build/**',
|
|
72
|
+
'**/.next/**',
|
|
73
|
+
'**/out/**',
|
|
74
|
+
'**/coverage/**',
|
|
75
|
+
],
|
|
76
|
+
absolute: false,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
for (const filePath of filePaths) {
|
|
80
|
+
const fullPath = join(projectPath, filePath);
|
|
81
|
+
const stats = statSync(fullPath);
|
|
82
|
+
|
|
83
|
+
if (stats.isFile()) {
|
|
84
|
+
const fileNode: FileNode = {
|
|
85
|
+
id: crypto.randomUUID(),
|
|
86
|
+
path: filePath, // Actual relative path for rule evaluation
|
|
87
|
+
path_hash: hashString(filePath),
|
|
88
|
+
depth: filePath.split(/[\\/]/).length,
|
|
89
|
+
size_bytes: stats.size,
|
|
90
|
+
estimated_lines: Math.floor(stats.size / 50), // Rough estimate
|
|
91
|
+
extension: filePath.split('.').pop() || '',
|
|
92
|
+
is_binary: isBinaryFile(filePath),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
files.push(fileNode);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Sort deterministically by path_hash
|
|
100
|
+
return files.sort((a, b) => a.path_hash.localeCompare(b.path_hash));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function scanDependencies(projectPath: string): Promise<DependencyNode[]> {
|
|
104
|
+
const dependencies: DependencyNode[] = [];
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const packageJsonPath = join(projectPath, 'package.json');
|
|
108
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
109
|
+
|
|
110
|
+
const processDeps = (
|
|
111
|
+
deps: Record<string, string>,
|
|
112
|
+
category: 'dev' | 'prod' | 'peer'
|
|
113
|
+
) => {
|
|
114
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
115
|
+
const versionMatch = version.match(/^(\d+)\.(\d+)/);
|
|
116
|
+
const major = versionMatch ? parseInt(versionMatch[1]) : 0;
|
|
117
|
+
const minor = versionMatch ? parseInt(versionMatch[2]) : 0;
|
|
118
|
+
|
|
119
|
+
dependencies.push({
|
|
120
|
+
id: crypto.randomUUID(),
|
|
121
|
+
name_hash: hashString(name),
|
|
122
|
+
major_version: major,
|
|
123
|
+
minor_version: minor,
|
|
124
|
+
category,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (packageJson.dependencies) {
|
|
130
|
+
processDeps(packageJson.dependencies, 'prod');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (packageJson.devDependencies) {
|
|
134
|
+
processDeps(packageJson.devDependencies, 'dev');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (packageJson.peerDependencies) {
|
|
138
|
+
processDeps(packageJson.peerDependencies, 'peer');
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
// No package.json found
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Sort deterministically by name_hash
|
|
145
|
+
return dependencies.sort((a, b) => a.name_hash.localeCompare(b.name_hash));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function calculateLanguageBreakdown(files: FileNode[]): Record<string, number> {
|
|
149
|
+
const breakdown: Record<string, number> = {};
|
|
150
|
+
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
const ext = file.extension.toLowerCase();
|
|
153
|
+
const lang = getLanguageFromExtension(ext);
|
|
154
|
+
breakdown[lang] = (breakdown[lang] || 0) + 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return breakdown;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getLanguageFromExtension(ext: string): string {
|
|
161
|
+
const map: Record<string, string> = {
|
|
162
|
+
ts: 'TypeScript',
|
|
163
|
+
js: 'JavaScript',
|
|
164
|
+
tsx: 'TypeScript',
|
|
165
|
+
jsx: 'JavaScript',
|
|
166
|
+
py: 'Python',
|
|
167
|
+
go: 'Go',
|
|
168
|
+
rs: 'Rust',
|
|
169
|
+
java: 'Java',
|
|
170
|
+
kt: 'Kotlin',
|
|
171
|
+
rb: 'Ruby',
|
|
172
|
+
php: 'PHP',
|
|
173
|
+
cs: 'C#',
|
|
174
|
+
cpp: 'C++',
|
|
175
|
+
c: 'C',
|
|
176
|
+
h: 'C',
|
|
177
|
+
json: 'JSON',
|
|
178
|
+
yaml: 'YAML',
|
|
179
|
+
yml: 'YAML',
|
|
180
|
+
md: 'Markdown',
|
|
181
|
+
html: 'HTML',
|
|
182
|
+
css: 'CSS',
|
|
183
|
+
scss: 'SCSS',
|
|
184
|
+
sql: 'SQL',
|
|
185
|
+
sh: 'Shell',
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return map[ext] || 'Other';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isBinaryFile(filePath: string): boolean {
|
|
192
|
+
const binaryExtensions = [
|
|
193
|
+
'.exe',
|
|
194
|
+
'.dll',
|
|
195
|
+
'.so',
|
|
196
|
+
'.dylib',
|
|
197
|
+
'.bin',
|
|
198
|
+
'.png',
|
|
199
|
+
'.jpg',
|
|
200
|
+
'.jpeg',
|
|
201
|
+
'.gif',
|
|
202
|
+
'.pdf',
|
|
203
|
+
'.zip',
|
|
204
|
+
'.tar',
|
|
205
|
+
'.gz',
|
|
206
|
+
'.7z',
|
|
207
|
+
'.woff',
|
|
208
|
+
'.woff2',
|
|
209
|
+
'.ttf',
|
|
210
|
+
'.eot',
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
214
|
+
return ext ? binaryExtensions.includes('.' + ext) : false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function hashString(str: string): string {
|
|
218
|
+
return crypto.createHash('sha256').update(str).digest('hex');
|
|
219
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
|
|
6
|
+
const supabaseUrl = process.env.SUPABASE_URL || 'https://qhonrrojtqklvlkvfswb.supabase.co';
|
|
7
|
+
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY || ''; // Must be set in environment
|
|
8
|
+
|
|
9
|
+
// Fetch projects from Supabase
|
|
10
|
+
async function fetchProjects(): Promise<any[]> {
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(`${supabaseUrl}/rest/v1/projects?select=id,name,created_at&order=created_at.desc`, {
|
|
13
|
+
headers: { 'apikey': supabaseAnonKey, 'Authorization': `Bearer ${supabaseAnonKey}` },
|
|
14
|
+
});
|
|
15
|
+
if (res.ok) {
|
|
16
|
+
const data = await res.json() as any[];
|
|
17
|
+
return data;
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
} catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Create new project in Supabase
|
|
26
|
+
async function createProject(name: string): Promise<string | null> {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`${supabaseUrl}/rest/v1/projects`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'apikey': supabaseAnonKey,
|
|
33
|
+
'Authorization': `Bearer ${supabaseAnonKey}`,
|
|
34
|
+
'Prefer': 'return=representation',
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
name,
|
|
38
|
+
user_id: '00000000-0000-0000-0000-000000000001', // Default user
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
if (res.ok) {
|
|
42
|
+
const data = await res.json() as any[];
|
|
43
|
+
return data[0]?.id || null;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Interactive prompt
|
|
52
|
+
function askQuestion(question: string): Promise<string> {
|
|
53
|
+
const rl = readline.createInterface({
|
|
54
|
+
input: process.stdin,
|
|
55
|
+
output: process.stdout,
|
|
56
|
+
});
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
rl.question(question, (answer) => {
|
|
59
|
+
rl.close();
|
|
60
|
+
resolve(answer);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Select or create project
|
|
66
|
+
async function selectOrCreateProject(): Promise<string | null> {
|
|
67
|
+
console.log(chalk.blue('\n📁 Loading projects...\n'));
|
|
68
|
+
|
|
69
|
+
const projects = await fetchProjects();
|
|
70
|
+
|
|
71
|
+
if (projects.length === 0) {
|
|
72
|
+
console.log(chalk.yellow('No projects found.\n'));
|
|
73
|
+
const answer = await askQuestion('Do you want to create a new project? (y/n): ');
|
|
74
|
+
if (answer.toLowerCase() === 'y') {
|
|
75
|
+
const name = await askQuestion('Enter project name: ');
|
|
76
|
+
if (name.trim()) {
|
|
77
|
+
const projectId = await createProject(name.trim());
|
|
78
|
+
if (projectId) {
|
|
79
|
+
console.log(chalk.green(`\n✅ Project "${name}" created successfully!`));
|
|
80
|
+
return projectId;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Display projects
|
|
88
|
+
console.log(chalk.white('Select a project:\n'));
|
|
89
|
+
console.log(chalk.gray('0. [+] Create new project\n'));
|
|
90
|
+
|
|
91
|
+
projects.forEach((p, i) => {
|
|
92
|
+
console.log(chalk.white(`${i + 1}. ${p.name}`));
|
|
93
|
+
console.log(chalk.gray(` ID: ${p.id}`));
|
|
94
|
+
console.log(chalk.gray(` Created: ${new Date(p.created_at).toLocaleDateString()}\n`));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const answer = await askQuestion('Enter number (0-' + projects.length + '): ');
|
|
98
|
+
const choice = parseInt(answer, 10);
|
|
99
|
+
|
|
100
|
+
if (choice === 0) {
|
|
101
|
+
// Create new project
|
|
102
|
+
const name = await askQuestion('\nEnter project name: ');
|
|
103
|
+
if (name.trim()) {
|
|
104
|
+
const projectId = await createProject(name.trim());
|
|
105
|
+
if (projectId) {
|
|
106
|
+
console.log(chalk.green(`\n✅ Project "${name}" created successfully!`));
|
|
107
|
+
return projectId;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
} else if (choice > 0 && choice <= projects.length) {
|
|
112
|
+
// Select existing project
|
|
113
|
+
const selected = projects[choice - 1];
|
|
114
|
+
console.log(chalk.green(`\n✅ Selected project: "${selected.name}"`));
|
|
115
|
+
return selected.id;
|
|
116
|
+
} else {
|
|
117
|
+
console.log(chalk.red('\n❌ Invalid selection'));
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function uploadCommand(options: {
|
|
123
|
+
file: string;
|
|
124
|
+
projectId?: string;
|
|
125
|
+
apiKey?: string;
|
|
126
|
+
}): Promise<void> {
|
|
127
|
+
const spinner = ora('Preparing upload...').start();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const fs = await import('fs/promises');
|
|
131
|
+
const snapshotData = await fs.readFile(options.file, 'utf-8');
|
|
132
|
+
const snapshot = JSON.parse(snapshotData);
|
|
133
|
+
|
|
134
|
+
// Determine project ID
|
|
135
|
+
let projectId: string | undefined = options.projectId;
|
|
136
|
+
|
|
137
|
+
if (!projectId) {
|
|
138
|
+
spinner.stop();
|
|
139
|
+
const selectedProjectId = await selectOrCreateProject();
|
|
140
|
+
if (!selectedProjectId) {
|
|
141
|
+
console.log(chalk.red('\n❌ No project selected. Exiting.'));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
projectId = selectedProjectId;
|
|
145
|
+
spinner.start('Uploading snapshot...');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 1. Create snapshot
|
|
149
|
+
spinner.text = 'Creating snapshot...';
|
|
150
|
+
const snapshotRes = await fetch(`${supabaseUrl}/rest/v1/snapshots`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
'apikey': supabaseAnonKey,
|
|
155
|
+
'Authorization': `Bearer ${supabaseAnonKey}`,
|
|
156
|
+
'Prefer': 'return=representation',
|
|
157
|
+
},
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
project_id: projectId,
|
|
160
|
+
file_count: snapshot.metadata.total_files,
|
|
161
|
+
size_mb: snapshot.metadata.total_size_bytes / (1024 * 1024),
|
|
162
|
+
storage_path: 'local://' + projectId,
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!snapshotRes.ok) {
|
|
167
|
+
const error = await snapshotRes.json() as any;
|
|
168
|
+
spinner.fail('Failed to create snapshot');
|
|
169
|
+
console.error(chalk.red(`Error: ${error.message || error.details || 'Unknown error'}`));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const snapshotData2 = await snapshotRes.json() as any[];
|
|
174
|
+
const snapshotId = snapshotData2[0]?.id || snapshotData2[0]?.id;
|
|
175
|
+
|
|
176
|
+
// 2. Run analysis locally
|
|
177
|
+
spinner.text = 'Analyzing snapshot...';
|
|
178
|
+
const { CoreEngine } = await import('@veestack-tools/engine');
|
|
179
|
+
const { RULES } = await import('@veestack-tools/engine');
|
|
180
|
+
|
|
181
|
+
const engine = new CoreEngine();
|
|
182
|
+
RULES.forEach((rule: any) => engine.registerRule(rule));
|
|
183
|
+
const result = await engine.analyze(snapshot);
|
|
184
|
+
|
|
185
|
+
if (!result.success) {
|
|
186
|
+
spinner.fail('Analysis failed');
|
|
187
|
+
console.error(chalk.red('Error:', result.error));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 3. Create report
|
|
192
|
+
spinner.text = 'Creating report...';
|
|
193
|
+
const reportRes = await fetch(`${supabaseUrl}/rest/v1/reports`, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: {
|
|
196
|
+
'Content-Type': 'application/json',
|
|
197
|
+
'apikey': supabaseAnonKey,
|
|
198
|
+
'Authorization': `Bearer ${supabaseAnonKey}`,
|
|
199
|
+
'Prefer': 'return=representation',
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
snapshot_id: snapshotId,
|
|
203
|
+
score: result.report.total_score,
|
|
204
|
+
issues_count: result.report.summary.total_findings,
|
|
205
|
+
critical_count: result.report.summary.critical_count,
|
|
206
|
+
high_count: result.report.summary.high_count,
|
|
207
|
+
medium_count: result.report.summary.medium_count,
|
|
208
|
+
low_count: result.report.summary.low_count,
|
|
209
|
+
execution_time_ms: 0,
|
|
210
|
+
report_json: result.report,
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (!reportRes.ok) {
|
|
215
|
+
const error = await reportRes.json() as any;
|
|
216
|
+
spinner.fail('Failed to create report');
|
|
217
|
+
console.error(chalk.red(`Error: ${error.message || error.details || 'Unknown error'}`));
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const reportData = await reportRes.json() as any[];
|
|
222
|
+
const reportId = reportData[0]?.id || reportData[0]?.id;
|
|
223
|
+
|
|
224
|
+
spinner.succeed('Analysis complete');
|
|
225
|
+
|
|
226
|
+
console.log(chalk.green('\n✅ Report generated and saved to VeeStack'));
|
|
227
|
+
console.log(chalk.gray('Project ID:'), projectId);
|
|
228
|
+
console.log(chalk.gray('Snapshot ID:'), snapshotId);
|
|
229
|
+
console.log(chalk.gray('Report ID:'), reportId);
|
|
230
|
+
console.log(chalk.bold('\n📊 Score:'), result.report.total_score);
|
|
231
|
+
console.log(chalk.gray('Severity:'), result.report.severity_band);
|
|
232
|
+
console.log(chalk.gray('Findings:'), result.report.summary.total_findings);
|
|
233
|
+
|
|
234
|
+
console.log(chalk.blue('\n🔗 View your report at:'));
|
|
235
|
+
console.log(chalk.underline(`http://localhost:3001/reports/${reportId}?project=${projectId}`));
|
|
236
|
+
} catch (error) {
|
|
237
|
+
spinner.fail('Upload failed');
|
|
238
|
+
console.error(error);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { scanCommand } from './commands/scan';
|
|
5
|
+
import { uploadCommand } from './commands/upload';
|
|
6
|
+
import { loginCommand } from './commands/login';
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('veestack')
|
|
12
|
+
.description('VeeStack CLI - Technical Stack Visibility Tool')
|
|
13
|
+
.version('3.0.1');
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.command('scan')
|
|
17
|
+
.description('Scan a project directory and generate analysis')
|
|
18
|
+
.option('-p, --path <path>', 'Path to project directory', '.')
|
|
19
|
+
.option('-o, --output <path>', 'Output file path', 'snapshot.json')
|
|
20
|
+
.option('--ci', 'CI mode (no interactive prompts)')
|
|
21
|
+
.action(scanCommand);
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command('upload')
|
|
25
|
+
.description('Upload a snapshot to VeeStack server')
|
|
26
|
+
.option('-f, --file <path>', 'Snapshot file path', 'snapshot.json')
|
|
27
|
+
.option('-p, --project-id <id>', 'Project ID')
|
|
28
|
+
.action(uploadCommand);
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command('login')
|
|
32
|
+
.description('Authenticate with VeeStack server')
|
|
33
|
+
.option('-k, --key <apiKey>', 'API key')
|
|
34
|
+
.action(loginCommand);
|
|
35
|
+
|
|
36
|
+
program.parse();
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { WasmEngineLoader, type WasmSnapshot, type WasmFileInfo, type WasmDependency, type WasmMetadata } from '../wasm/loader.js';
|
|
5
|
+
|
|
6
|
+
const LICENSE_FILE = '.veestack/license.key';
|
|
7
|
+
const SESSION_FILE = '.veestack/session.json';
|
|
8
|
+
|
|
9
|
+
export interface LicenseData {
|
|
10
|
+
key: string;
|
|
11
|
+
hardware_id: string;
|
|
12
|
+
issued_at: string;
|
|
13
|
+
expires_at: string;
|
|
14
|
+
tier: 'free' | 'pro' | 'enterprise';
|
|
15
|
+
features: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class LicenseManager {
|
|
19
|
+
private projectPath: string;
|
|
20
|
+
private licenseData: LicenseData | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(projectPath: string = '.') {
|
|
23
|
+
this.projectPath = projectPath;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if valid license exists
|
|
28
|
+
*/
|
|
29
|
+
async hasValidLicense(): Promise<boolean> {
|
|
30
|
+
const licensePath = path.join(this.projectPath, LICENSE_FILE);
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(licensePath)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const data = fs.readFileSync(licensePath, 'utf-8');
|
|
38
|
+
this.licenseData = JSON.parse(data);
|
|
39
|
+
|
|
40
|
+
// Validate hardware binding
|
|
41
|
+
const currentHardwareId = this.generateHardwareId();
|
|
42
|
+
if (this.licenseData?.hardware_id !== currentHardwareId) {
|
|
43
|
+
console.warn('⚠️ License bound to different machine');
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check expiration
|
|
48
|
+
if (this.licenseData?.expires_at) {
|
|
49
|
+
const expiry = new Date(this.licenseData.expires_at);
|
|
50
|
+
if (expiry < new Date()) {
|
|
51
|
+
console.warn('⚠️ License has expired');
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return true;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('❌ Error reading license:', error);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get license key
|
|
65
|
+
*/
|
|
66
|
+
getLicenseKey(): string | null {
|
|
67
|
+
return this.licenseData?.key || null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get license tier
|
|
72
|
+
*/
|
|
73
|
+
getLicenseTier(): string {
|
|
74
|
+
return this.licenseData?.tier || 'free';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate license with remote server
|
|
79
|
+
*/
|
|
80
|
+
async validateWithServer(apiUrl: string, apiKey: string): Promise<boolean> {
|
|
81
|
+
if (!this.licenseData) return false;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(`${apiUrl}/functions/v1/validate-license`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
'Authorization': `Bearer ${apiKey}`
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
license_key: this.licenseData.key,
|
|
92
|
+
hardware_id: this.licenseData.hardware_id
|
|
93
|
+
})
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!response.ok) return false;
|
|
97
|
+
|
|
98
|
+
const result = await response.json() as { valid: boolean };
|
|
99
|
+
return result.valid === true;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('❌ License validation error:', error);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate hardware fingerprint
|
|
108
|
+
*/
|
|
109
|
+
generateHardwareId(): string {
|
|
110
|
+
const components = [
|
|
111
|
+
process.platform,
|
|
112
|
+
process.arch,
|
|
113
|
+
process.version,
|
|
114
|
+
require('os').hostname(),
|
|
115
|
+
require('os').userInfo().username,
|
|
116
|
+
'veestack-v1'
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const combined = components.join('|');
|
|
120
|
+
return crypto.createHash('sha256').update(combined).digest('hex');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Save license to file
|
|
125
|
+
*/
|
|
126
|
+
async saveLicense(licenseData: LicenseData): Promise<void> {
|
|
127
|
+
const licenseDir = path.dirname(path.join(this.projectPath, LICENSE_FILE));
|
|
128
|
+
|
|
129
|
+
if (!fs.existsSync(licenseDir)) {
|
|
130
|
+
fs.mkdirSync(licenseDir, { recursive: true });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fs.writeFileSync(
|
|
134
|
+
path.join(this.projectPath, LICENSE_FILE),
|
|
135
|
+
JSON.stringify(licenseData, null, 2)
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
this.licenseData = licenseData;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get or create session
|
|
143
|
+
*/
|
|
144
|
+
getOrCreateSession(): { sessionId: string; isNew: boolean } {
|
|
145
|
+
const sessionPath = path.join(this.projectPath, SESSION_FILE);
|
|
146
|
+
|
|
147
|
+
if (fs.existsSync(sessionPath)) {
|
|
148
|
+
try {
|
|
149
|
+
const data = JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
|
|
150
|
+
|
|
151
|
+
// Check if session is still valid (24 hours)
|
|
152
|
+
const created = new Date(data.created_at);
|
|
153
|
+
const now = new Date();
|
|
154
|
+
const hoursDiff = (now.getTime() - created.getTime()) / (1000 * 60 * 60);
|
|
155
|
+
|
|
156
|
+
if (hoursDiff < 24) {
|
|
157
|
+
return { sessionId: data.session_id, isNew: false };
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// Invalid session file, create new
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Create new session
|
|
165
|
+
const sessionId = crypto.randomUUID();
|
|
166
|
+
const sessionData = {
|
|
167
|
+
session_id: sessionId,
|
|
168
|
+
created_at: new Date().toISOString(),
|
|
169
|
+
hardware_id: this.generateHardwareId()
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const sessionDir = path.dirname(sessionPath);
|
|
173
|
+
if (!fs.existsSync(sessionDir)) {
|
|
174
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fs.writeFileSync(sessionPath, JSON.stringify(sessionData, null, 2));
|
|
178
|
+
|
|
179
|
+
return { sessionId, isNew: true };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export interface WasmEngineConfig {
|
|
10
|
+
apiUrl: string;
|
|
11
|
+
apiKey: string;
|
|
12
|
+
licenseKey?: string;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RemoteRulesResponse {
|
|
17
|
+
success: boolean;
|
|
18
|
+
rules_version: string;
|
|
19
|
+
rules_count: number;
|
|
20
|
+
encrypted_rules: string;
|
|
21
|
+
encrypted_session_key: string;
|
|
22
|
+
expires_at: string;
|
|
23
|
+
licensed: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WasmAnalysisResult {
|
|
27
|
+
findings: WasmFinding[];
|
|
28
|
+
metrics: WasmMetrics;
|
|
29
|
+
execution_time_ms: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WasmFinding {
|
|
33
|
+
rule_id: string;
|
|
34
|
+
file_path: string;
|
|
35
|
+
line_number: number;
|
|
36
|
+
severity: 'critical' | 'high' | 'medium' | 'low' | 'info';
|
|
37
|
+
message: string;
|
|
38
|
+
context_hash: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface WasmMetrics {
|
|
42
|
+
files_scanned: number;
|
|
43
|
+
rules_applied: number;
|
|
44
|
+
patterns_matched: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WasmSnapshot {
|
|
48
|
+
version: string;
|
|
49
|
+
project_id: string;
|
|
50
|
+
files: WasmFileInfo[];
|
|
51
|
+
dependencies: WasmDependency[];
|
|
52
|
+
metadata: WasmMetadata;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WasmFileInfo {
|
|
56
|
+
path: string;
|
|
57
|
+
path_hash: string;
|
|
58
|
+
content_hash: string;
|
|
59
|
+
extension: string;
|
|
60
|
+
size_bytes: number;
|
|
61
|
+
estimated_lines: number;
|
|
62
|
+
depth: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface WasmDependency {
|
|
66
|
+
name_hash: string;
|
|
67
|
+
version_major: number;
|
|
68
|
+
version_minor: number;
|
|
69
|
+
category: 'dev' | 'prod' | 'peer';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface WasmMetadata {
|
|
73
|
+
total_files: number;
|
|
74
|
+
total_dependencies: number;
|
|
75
|
+
framework: string;
|
|
76
|
+
language: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class WasmEngineLoader {
|
|
80
|
+
private config: WasmEngineConfig;
|
|
81
|
+
private wasmModule: WebAssembly.Module | null = null;
|
|
82
|
+
private wasmInstance: WebAssembly.Instance | null = null;
|
|
83
|
+
private sessionKey: string = '';
|
|
84
|
+
private rules: any[] = [];
|
|
85
|
+
|
|
86
|
+
constructor(config: WasmEngineConfig) {
|
|
87
|
+
this.config = config;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Load and initialize the WASM engine
|
|
92
|
+
*/
|
|
93
|
+
async initialize(): Promise<boolean> {
|
|
94
|
+
try {
|
|
95
|
+
// Find WASM file path
|
|
96
|
+
const wasmPath = this.findWasmFile();
|
|
97
|
+
|
|
98
|
+
if (!wasmPath) {
|
|
99
|
+
console.error('❌ WASM engine not found');
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Read WASM file
|
|
104
|
+
const wasmBuffer = fs.readFileSync(wasmPath);
|
|
105
|
+
|
|
106
|
+
// Compile WASM module
|
|
107
|
+
this.wasmModule = await WebAssembly.compile(wasmBuffer);
|
|
108
|
+
|
|
109
|
+
// Create import object with required imports
|
|
110
|
+
const importObject = {
|
|
111
|
+
env: {
|
|
112
|
+
memory: new WebAssembly.Memory({ initial: 256, maximum: 512 }),
|
|
113
|
+
__memory_base: 0,
|
|
114
|
+
__table_base: 0,
|
|
115
|
+
abort: (msg: number, file: number, line: number, column: number) => {
|
|
116
|
+
console.error(`WASM abort: ${msg} at ${file}:${line}:${column}`);
|
|
117
|
+
},
|
|
118
|
+
// Add console.log for debugging
|
|
119
|
+
log: (ptr: number, len: number) => {
|
|
120
|
+
const memory = (this.wasmInstance?.exports.memory as WebAssembly.Memory);
|
|
121
|
+
if (memory) {
|
|
122
|
+
const bytes = new Uint8Array(memory.buffer, ptr, len);
|
|
123
|
+
const message = new TextDecoder().decode(bytes);
|
|
124
|
+
console.log(`[WASM] ${message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
console: {
|
|
129
|
+
log: (message: string) => {
|
|
130
|
+
console.log(`[WASM Engine] ${message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Instantiate WASM module
|
|
136
|
+
this.wasmInstance = await WebAssembly.instantiate(this.wasmModule, importObject);
|
|
137
|
+
|
|
138
|
+
console.log('✅ WASM engine initialized successfully');
|
|
139
|
+
return true;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('❌ Failed to initialize WASM engine:', error);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Fetch encrypted rules from remote server
|
|
148
|
+
*/
|
|
149
|
+
async fetchRemoteRules(framework: string, language: string): Promise<boolean> {
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetch(`${this.config.apiUrl}/functions/v1/rules`, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: {
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
'Authorization': `Bearer ${this.config.apiKey}`
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
framework,
|
|
159
|
+
language,
|
|
160
|
+
version: '1.0.0',
|
|
161
|
+
session_id: this.config.sessionId,
|
|
162
|
+
license_key: this.config.licenseKey
|
|
163
|
+
})
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
const error = await response.text();
|
|
168
|
+
console.error('❌ Failed to fetch rules:', error);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const data: RemoteRulesResponse = await response.json();
|
|
173
|
+
|
|
174
|
+
if (!data.success) {
|
|
175
|
+
console.error('❌ Rules API returned unsuccessful response');
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Decrypt session key
|
|
180
|
+
this.sessionKey = this.decryptSessionKey(data.encrypted_session_key);
|
|
181
|
+
|
|
182
|
+
// Decrypt rules
|
|
183
|
+
const decryptedRules = this.decryptRules(data.encrypted_rules);
|
|
184
|
+
this.rules = JSON.parse(decryptedRules);
|
|
185
|
+
|
|
186
|
+
console.log(`✅ Loaded ${this.rules.length} security rules (v${data.rules_version})`);
|
|
187
|
+
|
|
188
|
+
return true;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error('❌ Error fetching remote rules:', error);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Analyze a snapshot using WASM engine
|
|
197
|
+
*/
|
|
198
|
+
async analyzeSnapshot(
|
|
199
|
+
snapshot: WasmSnapshot,
|
|
200
|
+
fileContents: Map<string, string>
|
|
201
|
+
): Promise<WasmAnalysisResult | null> {
|
|
202
|
+
if (!this.wasmInstance) {
|
|
203
|
+
console.error('❌ WASM engine not initialized');
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const exports = this.wasmInstance.exports as any;
|
|
209
|
+
|
|
210
|
+
// Initialize session in WASM
|
|
211
|
+
const encryptedKey = this.encryptString(this.sessionKey);
|
|
212
|
+
|
|
213
|
+
// Convert strings to WASM memory
|
|
214
|
+
const snapshotJson = JSON.stringify(snapshot);
|
|
215
|
+
const contentsJson = JSON.stringify(Object.fromEntries(fileContents));
|
|
216
|
+
|
|
217
|
+
// Allocate memory for input
|
|
218
|
+
const snapshotPtr = this.allocateString(snapshotJson);
|
|
219
|
+
const contentsPtr = this.allocateString(contentsJson);
|
|
220
|
+
const sessionPtr = this.allocateString(encryptedKey);
|
|
221
|
+
|
|
222
|
+
// Call WASM functions
|
|
223
|
+
const initResult = exports.initialize_session(sessionPtr);
|
|
224
|
+
if (!initResult) {
|
|
225
|
+
console.error('❌ Failed to initialize WASM session');
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Load encrypted rules into WASM
|
|
230
|
+
const rulesJson = JSON.stringify(this.rules);
|
|
231
|
+
const encryptedRules = this.encryptString(rulesJson);
|
|
232
|
+
const rulesPtr = this.allocateString(encryptedRules);
|
|
233
|
+
|
|
234
|
+
const loadResult = exports.load_rules(rulesPtr);
|
|
235
|
+
if (loadResult !== 0) {
|
|
236
|
+
console.error('❌ Failed to load rules into WASM');
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Run analysis
|
|
241
|
+
const resultPtr = exports.analyze_snapshot(snapshotPtr, contentsPtr);
|
|
242
|
+
|
|
243
|
+
// Read result from WASM memory
|
|
244
|
+
const resultJson = this.readString(resultPtr);
|
|
245
|
+
|
|
246
|
+
// Free allocated memory (if WASM exports free function)
|
|
247
|
+
if (exports.free) {
|
|
248
|
+
exports.free(snapshotPtr);
|
|
249
|
+
exports.free(contentsPtr);
|
|
250
|
+
exports.free(sessionPtr);
|
|
251
|
+
exports.free(rulesPtr);
|
|
252
|
+
exports.free(resultPtr);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Parse result
|
|
256
|
+
const result: WasmAnalysisResult = JSON.parse(resultJson);
|
|
257
|
+
|
|
258
|
+
return result;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error('❌ Analysis error:', error);
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Validate license key
|
|
267
|
+
*/
|
|
268
|
+
validateLicense(hardwareId: string): boolean {
|
|
269
|
+
if (!this.wasmInstance) return false;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const exports = this.wasmInstance.exports as any;
|
|
273
|
+
const licensePtr = this.allocateString(this.config.licenseKey || '');
|
|
274
|
+
const hardwarePtr = this.allocateString(hardwareId);
|
|
275
|
+
|
|
276
|
+
const result = exports.validate_license(licensePtr, hardwarePtr);
|
|
277
|
+
|
|
278
|
+
return result === 1;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error('❌ License validation error:', error);
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Generate content fingerprint
|
|
287
|
+
*/
|
|
288
|
+
generateFingerprint(content: string): string {
|
|
289
|
+
if (!this.wasmInstance) {
|
|
290
|
+
// Fallback to Node.js crypto
|
|
291
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const exports = this.wasmInstance.exports as any;
|
|
296
|
+
const contentPtr = this.allocateString(content);
|
|
297
|
+
const resultPtr = exports.generate_fingerprint(contentPtr);
|
|
298
|
+
return this.readString(resultPtr);
|
|
299
|
+
} catch {
|
|
300
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Generate unique session ID
|
|
306
|
+
*/
|
|
307
|
+
static generateSessionId(): string {
|
|
308
|
+
return crypto.randomUUID();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Generate hardware fingerprint
|
|
313
|
+
*/
|
|
314
|
+
static generateHardwareId(): string {
|
|
315
|
+
const components = [
|
|
316
|
+
process.platform,
|
|
317
|
+
process.arch,
|
|
318
|
+
process.version,
|
|
319
|
+
require('os').hostname(),
|
|
320
|
+
'veestack'
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
const combined = components.join('|');
|
|
324
|
+
return crypto.createHash('sha256').update(combined).digest('hex');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Private helper methods
|
|
328
|
+
|
|
329
|
+
private findWasmFile(): string | null {
|
|
330
|
+
// Search for WASM file in multiple locations
|
|
331
|
+
const possiblePaths = [
|
|
332
|
+
path.join(__dirname, '../../wasm-engine/target/wasm32-unknown-unknown/release/veestack_wasm_engine.wasm'),
|
|
333
|
+
path.join(__dirname, '../engine.wasm'),
|
|
334
|
+
path.join(process.cwd(), 'node_modules/@veestack-tools/wasm-engine/engine.wasm'),
|
|
335
|
+
path.join(__dirname, '../../../wasm-engine/pkg/veestack_wasm_engine_bg.wasm'),
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
for (const wasmPath of possiblePaths) {
|
|
339
|
+
if (fs.existsSync(wasmPath)) {
|
|
340
|
+
return wasmPath;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private allocateString(str: string): number {
|
|
348
|
+
if (!this.wasmInstance) return 0;
|
|
349
|
+
|
|
350
|
+
const encoder = new TextEncoder();
|
|
351
|
+
const bytes = encoder.encode(str + '\0');
|
|
352
|
+
|
|
353
|
+
const exports = this.wasmInstance.exports as any;
|
|
354
|
+
const ptr = exports.malloc ? exports.malloc(bytes.length) : 0;
|
|
355
|
+
|
|
356
|
+
if (ptr === 0) {
|
|
357
|
+
// Fallback: use memory directly
|
|
358
|
+
const memory = exports.memory as WebAssembly.Memory;
|
|
359
|
+
const view = new Uint8Array(memory.buffer);
|
|
360
|
+
const offset = 1024; // Use a fixed offset
|
|
361
|
+
view.set(bytes, offset);
|
|
362
|
+
return offset;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const memory = exports.memory as WebAssembly.Memory;
|
|
366
|
+
const view = new Uint8Array(memory.buffer);
|
|
367
|
+
view.set(bytes, ptr);
|
|
368
|
+
|
|
369
|
+
return ptr;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private readString(ptr: number): string {
|
|
373
|
+
if (!this.wasmInstance) return '';
|
|
374
|
+
|
|
375
|
+
const exports = this.wasmInstance.exports as any;
|
|
376
|
+
const memory = exports.memory as WebAssembly.Memory;
|
|
377
|
+
const view = new Uint8Array(memory.buffer);
|
|
378
|
+
|
|
379
|
+
// Find null terminator
|
|
380
|
+
let end = ptr;
|
|
381
|
+
while (view[end] !== 0 && end < view.length) {
|
|
382
|
+
end++;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const bytes = view.slice(ptr, end);
|
|
386
|
+
return new TextDecoder().decode(bytes);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private decryptSessionKey(encrypted: string): string {
|
|
390
|
+
const decoded = Buffer.from(encrypted, 'base64');
|
|
391
|
+
return decoded.toString('utf-8');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private decryptRules(encrypted: string): string {
|
|
395
|
+
const decoded = Buffer.from(encrypted, 'base64');
|
|
396
|
+
const key = Buffer.from(this.sessionKey);
|
|
397
|
+
|
|
398
|
+
const decrypted = decoded.map((byte, i) => byte ^ key[i % key.length]);
|
|
399
|
+
return decrypted.toString('utf-8');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private encryptString(str: string): string {
|
|
403
|
+
const key = Buffer.from('veestack_session_key_2024');
|
|
404
|
+
const data = Buffer.from(str);
|
|
405
|
+
|
|
406
|
+
const encrypted = data.map((byte, i) => byte ^ key[i % key.length]);
|
|
407
|
+
return encrypted.toString('base64');
|
|
408
|
+
}
|
|
409
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"types": ["node"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/index.ts'],
|
|
5
|
+
format: ['esm'],
|
|
6
|
+
dts: false,
|
|
7
|
+
clean: true,
|
|
8
|
+
external: ['@veestack/types', '@veestack/engine', '@veestack/utils'],
|
|
9
|
+
splitting: false,
|
|
10
|
+
// No banner here - we'll add shebang in package.json bin
|
|
11
|
+
});
|