@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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/index.js';
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
+ });