archicore 0.1.9 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -0,0 +1,380 @@
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
+ import { readFile, writeFile, mkdir } from 'fs/promises';
8
+ import { join } from 'path';
9
+ import { Logger } from '../../utils/logger.js';
10
+ import { db } from './database.js';
11
+ const DATA_DIR = '.archicore';
12
+ const AUDIT_FILE = 'audit-logs.json';
13
+ const MAX_LOGS_IN_MEMORY = 10000;
14
+ const MAX_LOGS_IN_FILE = 100000;
15
+ // Singleton instance
16
+ let instance = null;
17
+ export class AuditService {
18
+ dataDir = DATA_DIR;
19
+ // JSON fallback
20
+ logs = [];
21
+ jsonInitialized = false;
22
+ saveTimeout = null;
23
+ constructor(dataDir = DATA_DIR) {
24
+ if (instance) {
25
+ return instance;
26
+ }
27
+ this.dataDir = dataDir;
28
+ instance = this;
29
+ }
30
+ useDatabase() {
31
+ return db.isAvailable();
32
+ }
33
+ // ========== JSON FALLBACK METHODS ==========
34
+ async ensureJsonInitialized() {
35
+ if (this.jsonInitialized)
36
+ return;
37
+ try {
38
+ await mkdir(this.dataDir, { recursive: true });
39
+ }
40
+ catch { }
41
+ try {
42
+ const path = join(this.dataDir, AUDIT_FILE);
43
+ const data = await readFile(path, 'utf-8');
44
+ const parsed = JSON.parse(data);
45
+ this.logs = parsed.logs.map(log => ({
46
+ ...log,
47
+ timestamp: new Date(log.timestamp)
48
+ }));
49
+ }
50
+ catch {
51
+ this.logs = [];
52
+ }
53
+ this.jsonInitialized = true;
54
+ }
55
+ async saveJson() {
56
+ if (this.saveTimeout) {
57
+ clearTimeout(this.saveTimeout);
58
+ }
59
+ this.saveTimeout = setTimeout(async () => {
60
+ try {
61
+ const path = join(this.dataDir, AUDIT_FILE);
62
+ if (this.logs.length > MAX_LOGS_IN_FILE) {
63
+ this.logs = this.logs.slice(-MAX_LOGS_IN_FILE);
64
+ }
65
+ const data = {
66
+ logs: this.logs,
67
+ lastRotation: new Date()
68
+ };
69
+ await writeFile(path, JSON.stringify(data, null, 2));
70
+ }
71
+ catch (error) {
72
+ Logger.error('Failed to save audit logs:', error);
73
+ }
74
+ }, 1000);
75
+ }
76
+ // ========== HELPER METHODS ==========
77
+ generateId() {
78
+ return 'audit_' + Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 9);
79
+ }
80
+ getSeverity(action, success) {
81
+ if (action.startsWith('admin.') ||
82
+ action === 'auth.password_change' ||
83
+ action === 'user.delete' ||
84
+ action === 'api.key_revoke') {
85
+ return success ? 'warning' : 'critical';
86
+ }
87
+ if ((action === 'auth.login' && !success) ||
88
+ action === 'auth.register' ||
89
+ action === 'project.delete' ||
90
+ action === 'github.disconnect') {
91
+ return 'warning';
92
+ }
93
+ return 'info';
94
+ }
95
+ rowToEntry(row) {
96
+ return {
97
+ id: row.id,
98
+ timestamp: row.created_at,
99
+ userId: row.user_id || undefined,
100
+ username: row.username || undefined,
101
+ action: row.action,
102
+ severity: row.severity,
103
+ ip: row.ip || undefined,
104
+ userAgent: row.user_agent || undefined,
105
+ details: row.details || undefined,
106
+ success: row.success,
107
+ errorMessage: row.error_message || undefined
108
+ };
109
+ }
110
+ // ========== PUBLIC API ==========
111
+ /**
112
+ * Log an action
113
+ */
114
+ async log(params) {
115
+ const success = params.success !== false;
116
+ const id = this.generateId();
117
+ const severity = this.getSeverity(params.action, success);
118
+ if (this.useDatabase()) {
119
+ try {
120
+ await db.query(`INSERT INTO audit_logs (id, user_id, username, action, severity, ip, user_agent, details, success, error_message)
121
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, [
122
+ id,
123
+ params.userId || null,
124
+ params.username || null,
125
+ params.action,
126
+ severity,
127
+ params.ip || null,
128
+ params.userAgent || null,
129
+ params.details ? JSON.stringify(params.details) : null,
130
+ success,
131
+ params.errorMessage || null
132
+ ]);
133
+ const entry = {
134
+ id,
135
+ timestamp: new Date(),
136
+ userId: params.userId,
137
+ username: params.username,
138
+ action: params.action,
139
+ severity,
140
+ ip: params.ip,
141
+ userAgent: params.userAgent,
142
+ details: params.details,
143
+ success,
144
+ errorMessage: params.errorMessage
145
+ };
146
+ if (entry.severity === 'critical') {
147
+ Logger.warn(`[AUDIT] Critical event: ${entry.action} by ${entry.username || entry.userId || 'unknown'}`);
148
+ }
149
+ return entry;
150
+ }
151
+ catch (error) {
152
+ Logger.error('Audit log error:', error);
153
+ // Fall through to JSON fallback
154
+ }
155
+ }
156
+ // JSON fallback
157
+ await this.ensureJsonInitialized();
158
+ const entry = {
159
+ id,
160
+ timestamp: new Date(),
161
+ userId: params.userId,
162
+ username: params.username,
163
+ action: params.action,
164
+ severity,
165
+ ip: params.ip,
166
+ userAgent: params.userAgent,
167
+ details: params.details,
168
+ success,
169
+ errorMessage: params.errorMessage
170
+ };
171
+ this.logs.push(entry);
172
+ if (this.logs.length > MAX_LOGS_IN_MEMORY) {
173
+ this.logs = this.logs.slice(-MAX_LOGS_IN_MEMORY);
174
+ }
175
+ await this.saveJson();
176
+ if (entry.severity === 'critical') {
177
+ Logger.warn(`[AUDIT] Critical event: ${entry.action} by ${entry.username || entry.userId || 'unknown'}`);
178
+ }
179
+ return entry;
180
+ }
181
+ /**
182
+ * Query audit logs
183
+ */
184
+ async query(params = {}) {
185
+ if (this.useDatabase()) {
186
+ try {
187
+ const conditions = ['1=1'];
188
+ const values = [];
189
+ let paramIndex = 1;
190
+ if (params.userId) {
191
+ conditions.push(`user_id = $${paramIndex++}`);
192
+ values.push(params.userId);
193
+ }
194
+ if (params.action) {
195
+ conditions.push(`action = $${paramIndex++}`);
196
+ values.push(params.action);
197
+ }
198
+ if (params.severity) {
199
+ conditions.push(`severity = $${paramIndex++}`);
200
+ values.push(params.severity);
201
+ }
202
+ if (params.success !== undefined) {
203
+ conditions.push(`success = $${paramIndex++}`);
204
+ values.push(params.success);
205
+ }
206
+ if (params.startDate) {
207
+ conditions.push(`created_at >= $${paramIndex++}`);
208
+ values.push(params.startDate);
209
+ }
210
+ if (params.endDate) {
211
+ conditions.push(`created_at <= $${paramIndex++}`);
212
+ values.push(params.endDate);
213
+ }
214
+ const whereClause = conditions.join(' AND ');
215
+ // Get total count
216
+ const countResult = await db.query(`SELECT COUNT(*) as count FROM audit_logs WHERE ${whereClause}`, values);
217
+ const total = parseInt(countResult.rows[0].count, 10);
218
+ // Get paginated results
219
+ const limit = params.limit || 50;
220
+ const offset = params.offset || 0;
221
+ const result = await db.query(`SELECT * FROM audit_logs WHERE ${whereClause} ORDER BY created_at DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`, [...values, limit, offset]);
222
+ return {
223
+ logs: result.rows.map(row => this.rowToEntry(row)),
224
+ total
225
+ };
226
+ }
227
+ catch (error) {
228
+ Logger.error('Audit query error:', error);
229
+ return { logs: [], total: 0 };
230
+ }
231
+ }
232
+ // JSON fallback
233
+ await this.ensureJsonInitialized();
234
+ let filtered = [...this.logs];
235
+ if (params.userId) {
236
+ filtered = filtered.filter(log => log.userId === params.userId);
237
+ }
238
+ if (params.action) {
239
+ filtered = filtered.filter(log => log.action === params.action);
240
+ }
241
+ if (params.severity) {
242
+ filtered = filtered.filter(log => log.severity === params.severity);
243
+ }
244
+ if (params.success !== undefined) {
245
+ filtered = filtered.filter(log => log.success === params.success);
246
+ }
247
+ if (params.startDate) {
248
+ filtered = filtered.filter(log => log.timestamp >= params.startDate);
249
+ }
250
+ if (params.endDate) {
251
+ filtered = filtered.filter(log => log.timestamp <= params.endDate);
252
+ }
253
+ filtered.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
254
+ const total = filtered.length;
255
+ const offset = params.offset || 0;
256
+ const limit = params.limit || 50;
257
+ filtered = filtered.slice(offset, offset + limit);
258
+ return { logs: filtered, total };
259
+ }
260
+ /**
261
+ * Get recent logs for a user
262
+ */
263
+ async getUserLogs(userId, limit = 20) {
264
+ const result = await this.query({ userId, limit });
265
+ return result.logs;
266
+ }
267
+ /**
268
+ * Get failed login attempts for an IP
269
+ */
270
+ async getFailedLogins(ip, since) {
271
+ if (this.useDatabase()) {
272
+ try {
273
+ const result = await db.query(`SELECT COUNT(*) as count FROM audit_logs WHERE action = $1 AND success = false AND ip = $2 AND created_at >= $3`, ['auth.login', ip, since]);
274
+ return parseInt(result.rows[0].count, 10);
275
+ }
276
+ catch (error) {
277
+ Logger.error('Failed logins query error:', error);
278
+ return 0;
279
+ }
280
+ }
281
+ await this.ensureJsonInitialized();
282
+ return this.logs.filter(log => log.action === 'auth.login' &&
283
+ !log.success &&
284
+ log.ip === ip &&
285
+ log.timestamp >= since).length;
286
+ }
287
+ /**
288
+ * Get statistics
289
+ */
290
+ async getStats(days = 7) {
291
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
292
+ if (this.useDatabase()) {
293
+ try {
294
+ // Total actions
295
+ const totalResult = await db.query('SELECT COUNT(*) as count FROM audit_logs WHERE created_at >= $1', [since]);
296
+ const totalActions = parseInt(totalResult.rows[0].count, 10);
297
+ // Unique users
298
+ const usersResult = await db.query('SELECT COUNT(DISTINCT user_id) as count FROM audit_logs WHERE created_at >= $1 AND user_id IS NOT NULL', [since]);
299
+ const uniqueUsers = parseInt(usersResult.rows[0].count, 10);
300
+ // Failed logins
301
+ const failedResult = await db.query(`SELECT COUNT(*) as count FROM audit_logs WHERE action = 'auth.login' AND success = false AND created_at >= $1`, [since]);
302
+ const failedLogins = parseInt(failedResult.rows[0].count, 10);
303
+ // Critical events
304
+ const criticalResult = await db.query(`SELECT COUNT(*) as count FROM audit_logs WHERE severity = 'critical' AND created_at >= $1`, [since]);
305
+ const criticalEvents = parseInt(criticalResult.rows[0].count, 10);
306
+ // Action counts
307
+ const actionResult = await db.query('SELECT action, COUNT(*) as count FROM audit_logs WHERE created_at >= $1 GROUP BY action', [since]);
308
+ const actionCounts = {};
309
+ for (const row of actionResult.rows) {
310
+ actionCounts[row.action] = parseInt(row.count, 10);
311
+ }
312
+ return {
313
+ totalActions,
314
+ uniqueUsers,
315
+ failedLogins,
316
+ criticalEvents,
317
+ actionCounts
318
+ };
319
+ }
320
+ catch (error) {
321
+ Logger.error('Audit stats error:', error);
322
+ return {
323
+ totalActions: 0,
324
+ uniqueUsers: 0,
325
+ failedLogins: 0,
326
+ criticalEvents: 0,
327
+ actionCounts: {}
328
+ };
329
+ }
330
+ }
331
+ // JSON fallback
332
+ await this.ensureJsonInitialized();
333
+ const recentLogs = this.logs.filter(log => log.timestamp >= since);
334
+ const uniqueUsers = new Set(recentLogs.map(log => log.userId).filter(Boolean));
335
+ const actionCounts = {};
336
+ for (const log of recentLogs) {
337
+ actionCounts[log.action] = (actionCounts[log.action] || 0) + 1;
338
+ }
339
+ return {
340
+ totalActions: recentLogs.length,
341
+ uniqueUsers: uniqueUsers.size,
342
+ failedLogins: recentLogs.filter(log => log.action === 'auth.login' && !log.success).length,
343
+ criticalEvents: recentLogs.filter(log => log.severity === 'critical').length,
344
+ actionCounts
345
+ };
346
+ }
347
+ /**
348
+ * Clear old logs (retention policy)
349
+ */
350
+ async cleanup(retentionDays = 90) {
351
+ const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
352
+ if (this.useDatabase()) {
353
+ try {
354
+ const result = await db.query('DELETE FROM audit_logs WHERE created_at < $1', [cutoff]);
355
+ const removed = result.rowCount ?? 0;
356
+ if (removed > 0) {
357
+ Logger.info(`Audit logs cleanup: removed ${removed} old entries`);
358
+ }
359
+ return removed;
360
+ }
361
+ catch (error) {
362
+ Logger.error('Audit cleanup error:', error);
363
+ return 0;
364
+ }
365
+ }
366
+ // JSON fallback
367
+ await this.ensureJsonInitialized();
368
+ const before = this.logs.length;
369
+ this.logs = this.logs.filter(log => log.timestamp >= cutoff);
370
+ const removed = before - this.logs.length;
371
+ if (removed > 0) {
372
+ await this.saveJson();
373
+ Logger.info(`Audit logs cleanup: removed ${removed} old entries`);
374
+ }
375
+ return removed;
376
+ }
377
+ }
378
+ // Export singleton instance
379
+ export const auditService = new AuditService();
380
+ //# sourceMappingURL=audit-service.js.map
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Authentication Service for ArchiCore
3
+ *
4
+ * Supports PostgreSQL (primary) with JSON file fallback
3
5
  */
4
6
  import { User, SubscriptionTier, AuthResponse } from '../../types/user.js';
5
7
  export declare class AuthService {
@@ -7,13 +9,11 @@ export declare class AuthService {
7
9
  private dataDir;
8
10
  private users;
9
11
  private sessions;
10
- private initialized;
12
+ private jsonInitialized;
11
13
  constructor(dataDir?: string);
12
- /**
13
- * Get singleton instance of AuthService
14
- */
15
14
  static getInstance(): AuthService;
16
- private ensureInitialized;
15
+ private useDatabase;
16
+ private ensureJsonInitialized;
17
17
  private createDefaultAdmin;
18
18
  private saveUsers;
19
19
  private saveSessions;
@@ -21,6 +21,8 @@ export declare class AuthService {
21
21
  private createEmptyUsage;
22
22
  private generateToken;
23
23
  private sanitizeUser;
24
+ private rowToUser;
25
+ private dbCreateDefaultAdmin;
24
26
  register(email: string, username: string, password: string): Promise<AuthResponse>;
25
27
  login(email: string, password: string): Promise<AuthResponse>;
26
28
  oauthLogin(provider: 'github' | 'google', profile: {
@@ -41,5 +43,9 @@ export declare class AuthService {
41
43
  getAllUsers(): Promise<Omit<User, 'passwordHash'>[]>;
42
44
  deleteUser(userId: string): Promise<boolean>;
43
45
  updateUser(userId: string, updates: Partial<Pick<User, 'username' | 'email' | 'avatar'>>): Promise<boolean>;
46
+ /**
47
+ * Initialize database and create default admin if needed
48
+ */
49
+ initDatabase(): Promise<void>;
44
50
  }
45
51
  //# sourceMappingURL=auth-service.d.ts.map