archicore 0.2.0 → 0.2.2
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/cli/commands/init.js +37 -14
- package/dist/cli/commands/interactive.js +174 -21
- package/dist/github/github-service.d.ts +5 -1
- package/dist/github/github-service.js +21 -3
- package/dist/orchestrator/index.js +19 -0
- package/dist/semantic-memory/embedding-service.d.ts +8 -1
- package/dist/semantic-memory/embedding-service.js +141 -47
- package/dist/server/index.js +163 -1
- package/dist/server/routes/admin.js +439 -1
- package/dist/server/routes/api.js +17 -2
- package/dist/server/routes/auth.js +46 -0
- package/dist/server/routes/developer.js +1 -1
- package/dist/server/routes/device-auth.js +10 -1
- package/dist/server/routes/github.js +17 -4
- package/dist/server/routes/report-issue.d.ts +7 -0
- package/dist/server/routes/report-issue.js +307 -0
- package/dist/server/services/audit-service.d.ts +88 -0
- package/dist/server/services/audit-service.js +380 -0
- package/dist/server/services/auth-service.d.ts +32 -5
- package/dist/server/services/auth-service.js +347 -54
- package/dist/server/services/cache.d.ts +77 -0
- package/dist/server/services/cache.js +245 -0
- package/dist/server/services/database.d.ts +43 -0
- package/dist/server/services/database.js +221 -0
- package/dist/server/services/encryption.d.ts +48 -0
- package/dist/server/services/encryption.js +148 -0
- package/package.json +17 -2
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report Issue Routes
|
|
3
|
+
*
|
|
4
|
+
* API for submitting bug reports and feedback
|
|
5
|
+
*/
|
|
6
|
+
import { Router } from 'express';
|
|
7
|
+
import multer from 'multer';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import { Logger } from '../../utils/logger.js';
|
|
11
|
+
export const reportIssueRouter = Router();
|
|
12
|
+
// Directory for storing reports
|
|
13
|
+
const REPORTS_DIR = process.env.REPORTS_DIR || path.join('.archicore', 'reports');
|
|
14
|
+
// File signature (magic bytes) validation
|
|
15
|
+
const FILE_SIGNATURES = {
|
|
16
|
+
'image/png': { magic: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], ext: '.png' },
|
|
17
|
+
'image/jpeg': { magic: [0xFF, 0xD8, 0xFF], ext: '.jpg' },
|
|
18
|
+
'image/gif': { magic: [0x47, 0x49, 0x46, 0x38], ext: '.gif' }, // GIF87a or GIF89a
|
|
19
|
+
'application/pdf': { magic: [0x25, 0x50, 0x44, 0x46], ext: '.pdf' } // %PDF
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Validate file by checking magic bytes (file signature)
|
|
23
|
+
* Prevents MIME type spoofing attacks
|
|
24
|
+
*/
|
|
25
|
+
function validateFileSignature(buffer) {
|
|
26
|
+
for (const [mimeType, { magic, ext }] of Object.entries(FILE_SIGNATURES)) {
|
|
27
|
+
if (buffer.length >= magic.length) {
|
|
28
|
+
const matches = magic.every((byte, index) => buffer[index] === byte);
|
|
29
|
+
if (matches) {
|
|
30
|
+
return { valid: true, detectedType: mimeType, ext };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { valid: false, detectedType: null, ext: null };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Sanitize filename - remove path traversal attempts and dangerous characters
|
|
38
|
+
* @deprecated Use validated extension from FILE_SIGNATURES instead
|
|
39
|
+
*/
|
|
40
|
+
function _sanitizeFilename(filename) {
|
|
41
|
+
// Remove path components
|
|
42
|
+
const basename = path.basename(filename);
|
|
43
|
+
// Remove null bytes and other dangerous characters
|
|
44
|
+
return basename
|
|
45
|
+
.replace(/\0/g, '')
|
|
46
|
+
.replace(/[<>:"/\\|?*]/g, '_')
|
|
47
|
+
.replace(/\.{2,}/g, '.'); // Prevent ..
|
|
48
|
+
}
|
|
49
|
+
void _sanitizeFilename; // Prevent unused warning
|
|
50
|
+
// Multer config for screenshot uploads
|
|
51
|
+
const upload = multer({
|
|
52
|
+
storage: multer.memoryStorage(),
|
|
53
|
+
limits: {
|
|
54
|
+
fileSize: 10 * 1024 * 1024 // 10MB max
|
|
55
|
+
},
|
|
56
|
+
fileFilter: (_req, file, cb) => {
|
|
57
|
+
// First check - MIME type (can be spoofed, but filters obvious garbage)
|
|
58
|
+
const allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'application/pdf'];
|
|
59
|
+
if (!allowedTypes.includes(file.mimetype)) {
|
|
60
|
+
cb(new Error('Invalid file type. Allowed: PNG, JPG, GIF, PDF'));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Real validation happens after upload when we can check magic bytes
|
|
64
|
+
cb(null, true);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* POST /api/report-issue
|
|
69
|
+
* Submit a new issue report
|
|
70
|
+
*/
|
|
71
|
+
reportIssueRouter.post('/', upload.single('screenshot'), async (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
const { description, category, priority, email, additionalInfo } = req.body;
|
|
74
|
+
// Validate required fields
|
|
75
|
+
if (!description || !category || !priority) {
|
|
76
|
+
res.status(400).json({
|
|
77
|
+
error: 'Missing required fields'
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Whitelist validation - only allow known categories and priorities
|
|
82
|
+
const allowedCategories = ['bug', 'analysis', 'performance', 'ui', 'github', 'feature', 'other'];
|
|
83
|
+
const allowedPriorities = ['critical', 'high', 'medium', 'low'];
|
|
84
|
+
if (!allowedCategories.includes(category)) {
|
|
85
|
+
res.status(400).json({ error: 'Invalid category' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!allowedPriorities.includes(priority)) {
|
|
89
|
+
res.status(400).json({ error: 'Invalid priority' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Basic email validation if provided
|
|
93
|
+
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
94
|
+
res.status(400).json({ error: 'Invalid email format' });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Generate unique ID
|
|
98
|
+
const reportId = `report_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
99
|
+
// Ensure reports directory exists
|
|
100
|
+
await fs.mkdir(REPORTS_DIR, { recursive: true });
|
|
101
|
+
// Save screenshot if provided (with security validation)
|
|
102
|
+
let screenshotFilename;
|
|
103
|
+
if (req.file) {
|
|
104
|
+
// Validate file signature (magic bytes) - prevents MIME spoofing
|
|
105
|
+
const validation = validateFileSignature(req.file.buffer);
|
|
106
|
+
if (!validation.valid) {
|
|
107
|
+
Logger.warn(`Invalid file signature detected for report ${reportId}. Claimed: ${req.file.mimetype}`);
|
|
108
|
+
res.status(400).json({
|
|
109
|
+
error: 'Invalid file content',
|
|
110
|
+
message: 'File does not match expected format. Please upload a valid PNG, JPG, GIF, or PDF.'
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Use extension from validated file type, NOT from user input
|
|
115
|
+
const safeExt = validation.ext;
|
|
116
|
+
screenshotFilename = `${reportId}${safeExt}`;
|
|
117
|
+
const screenshotPath = path.join(REPORTS_DIR, 'screenshots', screenshotFilename);
|
|
118
|
+
// Verify path doesn't escape reports directory (defense in depth)
|
|
119
|
+
const resolvedPath = path.resolve(screenshotPath);
|
|
120
|
+
const screenshotsDir = path.resolve(REPORTS_DIR, 'screenshots');
|
|
121
|
+
if (!resolvedPath.startsWith(screenshotsDir)) {
|
|
122
|
+
Logger.warn(`Path traversal attempt detected for report ${reportId}`);
|
|
123
|
+
res.status(400).json({ error: 'Invalid request' });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await fs.mkdir(path.join(REPORTS_DIR, 'screenshots'), { recursive: true });
|
|
127
|
+
await fs.writeFile(screenshotPath, req.file.buffer);
|
|
128
|
+
Logger.debug(`Screenshot saved for report ${reportId}`);
|
|
129
|
+
}
|
|
130
|
+
// Create report object - only store safe, non-sensitive data
|
|
131
|
+
const report = {
|
|
132
|
+
id: reportId,
|
|
133
|
+
description: description.substring(0, 10000), // Limit description length
|
|
134
|
+
category,
|
|
135
|
+
priority,
|
|
136
|
+
email: email || undefined,
|
|
137
|
+
additionalInfo: additionalInfo?.substring(0, 5000) || undefined,
|
|
138
|
+
hasScreenshot: !!screenshotFilename,
|
|
139
|
+
screenshotFilename, // Only filename, not full path
|
|
140
|
+
timestamp: new Date().toISOString(),
|
|
141
|
+
status: 'new'
|
|
142
|
+
};
|
|
143
|
+
// Save report to JSON file
|
|
144
|
+
const reportPath = path.join(REPORTS_DIR, `${reportId}.json`);
|
|
145
|
+
await fs.writeFile(reportPath, JSON.stringify(report, null, 2));
|
|
146
|
+
Logger.info(`New issue report submitted: ${reportId} (${category}/${priority})`);
|
|
147
|
+
// Optional: Send notification (webhook, email, etc.)
|
|
148
|
+
await sendNotification(report);
|
|
149
|
+
res.json({
|
|
150
|
+
success: true,
|
|
151
|
+
reportId,
|
|
152
|
+
message: 'Report submitted successfully'
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
// Log full error internally, but don't expose details to client
|
|
157
|
+
Logger.error('Failed to submit report:', error);
|
|
158
|
+
if (error instanceof multer.MulterError) {
|
|
159
|
+
if (error.code === 'LIMIT_FILE_SIZE') {
|
|
160
|
+
res.status(413).json({ error: 'Screenshot too large. Maximum size is 10MB.' });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Generic error message - never expose internal error details
|
|
165
|
+
res.status(500).json({
|
|
166
|
+
error: 'Failed to submit report. Please try again later.'
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
/**
|
|
171
|
+
* GET /api/report-issue/list
|
|
172
|
+
* Get list of reports (admin only)
|
|
173
|
+
*/
|
|
174
|
+
reportIssueRouter.get('/list', async (req, res) => {
|
|
175
|
+
try {
|
|
176
|
+
// Basic auth check - in production use proper admin middleware
|
|
177
|
+
const adminKey = req.headers['x-admin-key'];
|
|
178
|
+
if (adminKey !== process.env.ADMIN_SECRET) {
|
|
179
|
+
res.status(403).json({ error: 'Admin access required' });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const files = await fs.readdir(REPORTS_DIR).catch(() => []);
|
|
183
|
+
const reports = [];
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
if (file.endsWith('.json')) {
|
|
186
|
+
const content = await fs.readFile(path.join(REPORTS_DIR, file), 'utf-8');
|
|
187
|
+
reports.push(JSON.parse(content));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Sort by timestamp descending
|
|
191
|
+
reports.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
192
|
+
res.json({ reports, total: reports.length });
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
Logger.error('Failed to list reports:', error);
|
|
196
|
+
res.status(500).json({ error: 'Failed to list reports' });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
/**
|
|
200
|
+
* PATCH /api/report-issue/:id/status
|
|
201
|
+
* Update report status (admin only)
|
|
202
|
+
*/
|
|
203
|
+
reportIssueRouter.patch('/:id/status', async (req, res) => {
|
|
204
|
+
try {
|
|
205
|
+
const adminKey = req.headers['x-admin-key'];
|
|
206
|
+
if (adminKey !== process.env.ADMIN_SECRET) {
|
|
207
|
+
res.status(403).json({ error: 'Admin access required' });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const { id } = req.params;
|
|
211
|
+
const { status } = req.body;
|
|
212
|
+
if (!['new', 'reviewed', 'resolved'].includes(status)) {
|
|
213
|
+
res.status(400).json({ error: 'Invalid status' });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const reportPath = path.join(REPORTS_DIR, `${id}.json`);
|
|
217
|
+
const content = await fs.readFile(reportPath, 'utf-8');
|
|
218
|
+
const report = JSON.parse(content);
|
|
219
|
+
report.status = status;
|
|
220
|
+
await fs.writeFile(reportPath, JSON.stringify(report, null, 2));
|
|
221
|
+
res.json({ success: true, report });
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
Logger.error('Failed to update report status:', error);
|
|
225
|
+
res.status(500).json({ error: 'Failed to update status' });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
/**
|
|
229
|
+
* Send notification about new report (optional integrations)
|
|
230
|
+
*/
|
|
231
|
+
async function sendNotification(report) {
|
|
232
|
+
// Discord webhook
|
|
233
|
+
const discordWebhook = process.env.DISCORD_WEBHOOK_URL;
|
|
234
|
+
if (discordWebhook) {
|
|
235
|
+
try {
|
|
236
|
+
const priorityColors = {
|
|
237
|
+
critical: 0xff0000,
|
|
238
|
+
high: 0xff6600,
|
|
239
|
+
medium: 0xffcc00,
|
|
240
|
+
low: 0x00cc00
|
|
241
|
+
};
|
|
242
|
+
await fetch(discordWebhook, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: { 'Content-Type': 'application/json' },
|
|
245
|
+
body: JSON.stringify({
|
|
246
|
+
embeds: [{
|
|
247
|
+
title: `New Issue Report: ${report.category}`,
|
|
248
|
+
description: report.description.substring(0, 500),
|
|
249
|
+
color: priorityColors[report.priority] || 0x808080,
|
|
250
|
+
fields: [
|
|
251
|
+
{ name: 'Priority', value: report.priority, inline: true },
|
|
252
|
+
{ name: 'Category', value: report.category, inline: true },
|
|
253
|
+
{ name: 'Report ID', value: report.id, inline: false }
|
|
254
|
+
],
|
|
255
|
+
timestamp: report.timestamp
|
|
256
|
+
}]
|
|
257
|
+
})
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
Logger.warn('Failed to send Discord notification:', err);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Slack webhook
|
|
265
|
+
const slackWebhook = process.env.SLACK_WEBHOOK_URL;
|
|
266
|
+
if (slackWebhook) {
|
|
267
|
+
try {
|
|
268
|
+
await fetch(slackWebhook, {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
headers: { 'Content-Type': 'application/json' },
|
|
271
|
+
body: JSON.stringify({
|
|
272
|
+
text: `*New Issue Report*\n*Category:* ${report.category}\n*Priority:* ${report.priority}\n*Description:* ${report.description.substring(0, 200)}...`
|
|
273
|
+
})
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
Logger.warn('Failed to send Slack notification:', err);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Telegram bot
|
|
281
|
+
const telegramToken = process.env.TELEGRAM_BOT_TOKEN;
|
|
282
|
+
const telegramChatId = process.env.TELEGRAM_CHAT_ID;
|
|
283
|
+
if (telegramToken && telegramChatId) {
|
|
284
|
+
try {
|
|
285
|
+
const priorityEmoji = {
|
|
286
|
+
critical: '🔴',
|
|
287
|
+
high: '🟠',
|
|
288
|
+
medium: '🟡',
|
|
289
|
+
low: '🟢'
|
|
290
|
+
};
|
|
291
|
+
const message = `${priorityEmoji[report.priority] || '⚪'} *New Issue Report*\n\n*Category:* ${report.category}\n*Priority:* ${report.priority}\n*Description:* ${report.description.substring(0, 300)}...\n\n_ID: ${report.id}_`;
|
|
292
|
+
await fetch(`https://api.telegram.org/bot${telegramToken}/sendMessage`, {
|
|
293
|
+
method: 'POST',
|
|
294
|
+
headers: { 'Content-Type': 'application/json' },
|
|
295
|
+
body: JSON.stringify({
|
|
296
|
+
chat_id: telegramChatId,
|
|
297
|
+
text: message,
|
|
298
|
+
parse_mode: 'Markdown'
|
|
299
|
+
})
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
Logger.warn('Failed to send Telegram notification:', err);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
//# sourceMappingURL=report-issue.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Log Service for ArchiCore
|
|
3
|
+
*
|
|
4
|
+
* Logs user actions for security and compliance purposes
|
|
5
|
+
* Supports PostgreSQL (primary) with JSON file fallback
|
|
6
|
+
*/
|
|
7
|
+
export type AuditAction = 'auth.login' | 'auth.logout' | 'auth.register' | 'auth.password_change' | 'auth.device_login' | 'user.update' | 'user.tier_change' | 'user.delete' | 'project.create' | 'project.delete' | 'project.analyze' | 'project.simulate' | 'project.ask' | 'project.docs' | 'github.connect' | 'github.disconnect' | 'github.repo_connect' | 'github.repo_disconnect' | 'github.webhook_received' | 'api.key_create' | 'api.key_revoke' | 'api.request' | 'admin.login' | 'admin.user_update' | 'admin.user_delete' | 'admin.tier_change' | 'admin.view_logs';
|
|
8
|
+
export type AuditSeverity = 'info' | 'warning' | 'critical';
|
|
9
|
+
export interface AuditLogEntry {
|
|
10
|
+
id: string;
|
|
11
|
+
timestamp: Date;
|
|
12
|
+
userId?: string;
|
|
13
|
+
username?: string;
|
|
14
|
+
action: AuditAction;
|
|
15
|
+
severity: AuditSeverity;
|
|
16
|
+
ip?: string;
|
|
17
|
+
userAgent?: string;
|
|
18
|
+
details?: Record<string, any>;
|
|
19
|
+
success: boolean;
|
|
20
|
+
errorMessage?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AuditLogQuery {
|
|
23
|
+
userId?: string;
|
|
24
|
+
action?: AuditAction;
|
|
25
|
+
severity?: AuditSeverity;
|
|
26
|
+
startDate?: Date;
|
|
27
|
+
endDate?: Date;
|
|
28
|
+
success?: boolean;
|
|
29
|
+
limit?: number;
|
|
30
|
+
offset?: number;
|
|
31
|
+
}
|
|
32
|
+
export declare class AuditService {
|
|
33
|
+
private dataDir;
|
|
34
|
+
private logs;
|
|
35
|
+
private jsonInitialized;
|
|
36
|
+
private saveTimeout;
|
|
37
|
+
constructor(dataDir?: string);
|
|
38
|
+
private useDatabase;
|
|
39
|
+
private ensureJsonInitialized;
|
|
40
|
+
private saveJson;
|
|
41
|
+
private generateId;
|
|
42
|
+
private getSeverity;
|
|
43
|
+
private rowToEntry;
|
|
44
|
+
/**
|
|
45
|
+
* Log an action
|
|
46
|
+
*/
|
|
47
|
+
log(params: {
|
|
48
|
+
userId?: string;
|
|
49
|
+
username?: string;
|
|
50
|
+
action: AuditAction;
|
|
51
|
+
ip?: string;
|
|
52
|
+
userAgent?: string;
|
|
53
|
+
details?: Record<string, any>;
|
|
54
|
+
success?: boolean;
|
|
55
|
+
errorMessage?: string;
|
|
56
|
+
}): Promise<AuditLogEntry>;
|
|
57
|
+
/**
|
|
58
|
+
* Query audit logs
|
|
59
|
+
*/
|
|
60
|
+
query(params?: AuditLogQuery): Promise<{
|
|
61
|
+
logs: AuditLogEntry[];
|
|
62
|
+
total: number;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Get recent logs for a user
|
|
66
|
+
*/
|
|
67
|
+
getUserLogs(userId: string, limit?: number): Promise<AuditLogEntry[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Get failed login attempts for an IP
|
|
70
|
+
*/
|
|
71
|
+
getFailedLogins(ip: string, since: Date): Promise<number>;
|
|
72
|
+
/**
|
|
73
|
+
* Get statistics
|
|
74
|
+
*/
|
|
75
|
+
getStats(days?: number): Promise<{
|
|
76
|
+
totalActions: number;
|
|
77
|
+
uniqueUsers: number;
|
|
78
|
+
failedLogins: number;
|
|
79
|
+
criticalEvents: number;
|
|
80
|
+
actionCounts: Record<string, number>;
|
|
81
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* Clear old logs (retention policy)
|
|
84
|
+
*/
|
|
85
|
+
cleanup(retentionDays?: number): Promise<number>;
|
|
86
|
+
}
|
|
87
|
+
export declare const auditService: AuditService;
|
|
88
|
+
//# sourceMappingURL=audit-service.d.ts.map
|