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,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArchiCore Redis Cache Service
|
|
3
|
+
*
|
|
4
|
+
* Provides caching for API responses to improve performance
|
|
5
|
+
*/
|
|
6
|
+
import Redis from 'ioredis';
|
|
7
|
+
import { Logger } from '../../utils/logger.js';
|
|
8
|
+
const DEFAULT_TTL = 300; // 5 minutes
|
|
9
|
+
const CACHE_PREFIX = 'archicore:';
|
|
10
|
+
class CacheService {
|
|
11
|
+
client = null;
|
|
12
|
+
enabled = false;
|
|
13
|
+
memoryCache = new Map();
|
|
14
|
+
async connect() {
|
|
15
|
+
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
|
|
16
|
+
try {
|
|
17
|
+
this.client = new Redis(redisUrl, {
|
|
18
|
+
maxRetriesPerRequest: 3,
|
|
19
|
+
retryStrategy: (times) => {
|
|
20
|
+
if (times > 3) {
|
|
21
|
+
Logger.warn('Redis connection failed, using memory cache fallback');
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return Math.min(times * 100, 3000);
|
|
25
|
+
},
|
|
26
|
+
lazyConnect: true,
|
|
27
|
+
});
|
|
28
|
+
await this.client.connect();
|
|
29
|
+
this.client.on('error', (err) => {
|
|
30
|
+
Logger.error('Redis error:', err.message);
|
|
31
|
+
this.enabled = false;
|
|
32
|
+
});
|
|
33
|
+
this.client.on('connect', () => {
|
|
34
|
+
Logger.info('Redis connected');
|
|
35
|
+
this.enabled = true;
|
|
36
|
+
});
|
|
37
|
+
this.client.on('close', () => {
|
|
38
|
+
Logger.warn('Redis connection closed');
|
|
39
|
+
this.enabled = false;
|
|
40
|
+
});
|
|
41
|
+
// Test connection
|
|
42
|
+
await this.client.ping();
|
|
43
|
+
this.enabled = true;
|
|
44
|
+
Logger.success('Redis cache enabled');
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
Logger.warn('Redis not available, using memory cache');
|
|
48
|
+
this.enabled = false;
|
|
49
|
+
this.client = null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
getKey(key, prefix) {
|
|
53
|
+
return `${CACHE_PREFIX}${prefix || ''}${key}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get value from cache
|
|
57
|
+
*/
|
|
58
|
+
async get(key, prefix) {
|
|
59
|
+
const fullKey = this.getKey(key, prefix);
|
|
60
|
+
// Try Redis first
|
|
61
|
+
if (this.enabled && this.client) {
|
|
62
|
+
try {
|
|
63
|
+
const data = await this.client.get(fullKey);
|
|
64
|
+
if (data) {
|
|
65
|
+
return JSON.parse(data);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
Logger.error('Cache get error:', error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Fallback to memory cache
|
|
73
|
+
const cached = this.memoryCache.get(fullKey);
|
|
74
|
+
if (cached && cached.expires > Date.now()) {
|
|
75
|
+
return cached.data;
|
|
76
|
+
}
|
|
77
|
+
// Clean up expired entry
|
|
78
|
+
if (cached) {
|
|
79
|
+
this.memoryCache.delete(fullKey);
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Set value in cache
|
|
85
|
+
*/
|
|
86
|
+
async set(key, value, options = {}) {
|
|
87
|
+
const fullKey = this.getKey(key, options.prefix);
|
|
88
|
+
const ttl = options.ttl || DEFAULT_TTL;
|
|
89
|
+
const serialized = JSON.stringify(value);
|
|
90
|
+
// Try Redis first
|
|
91
|
+
if (this.enabled && this.client) {
|
|
92
|
+
try {
|
|
93
|
+
await this.client.setex(fullKey, ttl, serialized);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
Logger.error('Cache set error:', error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Fallback to memory cache
|
|
101
|
+
this.memoryCache.set(fullKey, {
|
|
102
|
+
data: value,
|
|
103
|
+
expires: Date.now() + (ttl * 1000),
|
|
104
|
+
});
|
|
105
|
+
// Clean up old memory cache entries periodically
|
|
106
|
+
if (this.memoryCache.size > 1000) {
|
|
107
|
+
this.cleanupMemoryCache();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Delete value from cache
|
|
112
|
+
*/
|
|
113
|
+
async del(key, prefix) {
|
|
114
|
+
const fullKey = this.getKey(key, prefix);
|
|
115
|
+
if (this.enabled && this.client) {
|
|
116
|
+
try {
|
|
117
|
+
await this.client.del(fullKey);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
Logger.error('Cache del error:', error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
this.memoryCache.delete(fullKey);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Delete all keys matching pattern
|
|
127
|
+
*/
|
|
128
|
+
async delPattern(pattern) {
|
|
129
|
+
const fullPattern = this.getKey(pattern);
|
|
130
|
+
if (this.enabled && this.client) {
|
|
131
|
+
try {
|
|
132
|
+
const keys = await this.client.keys(fullPattern);
|
|
133
|
+
if (keys.length > 0) {
|
|
134
|
+
await this.client.del(...keys);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
Logger.error('Cache delPattern error:', error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Clean memory cache matching pattern
|
|
142
|
+
const regex = new RegExp(fullPattern.replace(/\*/g, '.*'));
|
|
143
|
+
for (const key of this.memoryCache.keys()) {
|
|
144
|
+
if (regex.test(key)) {
|
|
145
|
+
this.memoryCache.delete(key);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Clear all cache
|
|
151
|
+
*/
|
|
152
|
+
async flush() {
|
|
153
|
+
if (this.enabled && this.client) {
|
|
154
|
+
try {
|
|
155
|
+
const keys = await this.client.keys(`${CACHE_PREFIX}*`);
|
|
156
|
+
if (keys.length > 0) {
|
|
157
|
+
await this.client.del(...keys);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
Logger.error('Cache flush error:', error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
this.memoryCache.clear();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get or set cache with callback
|
|
168
|
+
*/
|
|
169
|
+
async getOrSet(key, callback, options = {}) {
|
|
170
|
+
const cached = await this.get(key, options.prefix);
|
|
171
|
+
if (cached !== null) {
|
|
172
|
+
return cached;
|
|
173
|
+
}
|
|
174
|
+
const value = await callback();
|
|
175
|
+
await this.set(key, value, options);
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Check if cache is available
|
|
180
|
+
*/
|
|
181
|
+
isEnabled() {
|
|
182
|
+
return this.enabled;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get cache stats
|
|
186
|
+
*/
|
|
187
|
+
async getStats() {
|
|
188
|
+
let keys = 0;
|
|
189
|
+
if (this.enabled && this.client) {
|
|
190
|
+
try {
|
|
191
|
+
const allKeys = await this.client.keys(`${CACHE_PREFIX}*`);
|
|
192
|
+
keys = allKeys.length;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
Logger.error('Cache stats error:', error);
|
|
196
|
+
}
|
|
197
|
+
return { enabled: true, type: 'redis', keys };
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
enabled: false,
|
|
201
|
+
type: 'memory',
|
|
202
|
+
keys: this.memoryCache.size
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Cleanup expired memory cache entries
|
|
207
|
+
*/
|
|
208
|
+
cleanupMemoryCache() {
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
for (const [key, value] of this.memoryCache.entries()) {
|
|
211
|
+
if (value.expires < now) {
|
|
212
|
+
this.memoryCache.delete(key);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Disconnect from Redis
|
|
218
|
+
*/
|
|
219
|
+
async disconnect() {
|
|
220
|
+
if (this.client) {
|
|
221
|
+
await this.client.quit();
|
|
222
|
+
this.client = null;
|
|
223
|
+
this.enabled = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Singleton instance
|
|
228
|
+
export const cache = new CacheService();
|
|
229
|
+
// Cache key generators
|
|
230
|
+
export const cacheKeys = {
|
|
231
|
+
project: (projectId) => `project:${projectId}`,
|
|
232
|
+
projectAnalysis: (projectId) => `analysis:${projectId}`,
|
|
233
|
+
projectMetrics: (projectId) => `metrics:${projectId}`,
|
|
234
|
+
projectSearch: (projectId, query) => `search:${projectId}:${query}`,
|
|
235
|
+
user: (userId) => `user:${userId}`,
|
|
236
|
+
userProjects: (userId) => `user-projects:${userId}`,
|
|
237
|
+
};
|
|
238
|
+
// TTL presets (in seconds)
|
|
239
|
+
export const cacheTTL = {
|
|
240
|
+
short: 60, // 1 minute
|
|
241
|
+
medium: 300, // 5 minutes
|
|
242
|
+
long: 3600, // 1 hour
|
|
243
|
+
day: 86400, // 24 hours
|
|
244
|
+
};
|
|
245
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Database Service for ArchiCore
|
|
3
|
+
*
|
|
4
|
+
* Connection pooling and schema management
|
|
5
|
+
*/
|
|
6
|
+
import pg from 'pg';
|
|
7
|
+
declare class DatabaseService {
|
|
8
|
+
private pool;
|
|
9
|
+
private initialized;
|
|
10
|
+
private initPromise;
|
|
11
|
+
/**
|
|
12
|
+
* Initialize database connection pool
|
|
13
|
+
*/
|
|
14
|
+
init(): Promise<void>;
|
|
15
|
+
private _init;
|
|
16
|
+
/**
|
|
17
|
+
* Initialize database schema
|
|
18
|
+
*/
|
|
19
|
+
private initSchema;
|
|
20
|
+
/**
|
|
21
|
+
* Check if database is available
|
|
22
|
+
*/
|
|
23
|
+
isAvailable(): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Get a client from the pool
|
|
26
|
+
*/
|
|
27
|
+
getClient(): Promise<pg.PoolClient>;
|
|
28
|
+
/**
|
|
29
|
+
* Execute a query
|
|
30
|
+
*/
|
|
31
|
+
query<T extends pg.QueryResultRow = any>(text: string, params?: any[]): Promise<pg.QueryResult<T>>;
|
|
32
|
+
/**
|
|
33
|
+
* Execute a transaction
|
|
34
|
+
*/
|
|
35
|
+
transaction<T>(callback: (client: pg.PoolClient) => Promise<T>): Promise<T>;
|
|
36
|
+
/**
|
|
37
|
+
* Close the connection pool
|
|
38
|
+
*/
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export declare const db: DatabaseService;
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Database Service for ArchiCore
|
|
3
|
+
*
|
|
4
|
+
* Connection pooling and schema management
|
|
5
|
+
*/
|
|
6
|
+
import pg from 'pg';
|
|
7
|
+
import { Logger } from '../../utils/logger.js';
|
|
8
|
+
const { Pool } = pg;
|
|
9
|
+
// Database schema version for migrations
|
|
10
|
+
const SCHEMA_VERSION = 1;
|
|
11
|
+
// SQL schema for ArchiCore
|
|
12
|
+
const SCHEMA_SQL = `
|
|
13
|
+
-- Users table
|
|
14
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
15
|
+
id VARCHAR(64) PRIMARY KEY,
|
|
16
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
17
|
+
username VARCHAR(100) NOT NULL,
|
|
18
|
+
password_hash VARCHAR(255),
|
|
19
|
+
avatar TEXT,
|
|
20
|
+
tier VARCHAR(20) NOT NULL DEFAULT 'free',
|
|
21
|
+
provider VARCHAR(20) NOT NULL DEFAULT 'email',
|
|
22
|
+
provider_id VARCHAR(255),
|
|
23
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
24
|
+
last_login_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
25
|
+
|
|
26
|
+
-- Usage tracking (reset daily)
|
|
27
|
+
requests_today INTEGER DEFAULT 0,
|
|
28
|
+
full_analysis_today INTEGER DEFAULT 0,
|
|
29
|
+
projects_today INTEGER DEFAULT 0,
|
|
30
|
+
usage_reset_date DATE DEFAULT CURRENT_DATE,
|
|
31
|
+
|
|
32
|
+
-- Subscription info
|
|
33
|
+
subscription_id VARCHAR(64),
|
|
34
|
+
subscription_status VARCHAR(20),
|
|
35
|
+
subscription_end_date TIMESTAMP WITH TIME ZONE
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- Sessions table
|
|
39
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
40
|
+
token VARCHAR(128) PRIMARY KEY,
|
|
41
|
+
user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
42
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
43
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
-- Audit logs table
|
|
47
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
48
|
+
id VARCHAR(64) PRIMARY KEY,
|
|
49
|
+
user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL,
|
|
50
|
+
username VARCHAR(100),
|
|
51
|
+
action VARCHAR(50) NOT NULL,
|
|
52
|
+
severity VARCHAR(20) NOT NULL DEFAULT 'info',
|
|
53
|
+
ip VARCHAR(45),
|
|
54
|
+
user_agent TEXT,
|
|
55
|
+
details JSONB,
|
|
56
|
+
success BOOLEAN DEFAULT true,
|
|
57
|
+
error_message TEXT,
|
|
58
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
-- Projects table (for future use)
|
|
62
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
63
|
+
id VARCHAR(64) PRIMARY KEY,
|
|
64
|
+
user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
65
|
+
name VARCHAR(255) NOT NULL,
|
|
66
|
+
description TEXT,
|
|
67
|
+
repo_url TEXT,
|
|
68
|
+
repo_provider VARCHAR(20),
|
|
69
|
+
repo_id VARCHAR(255),
|
|
70
|
+
webhook_secret TEXT,
|
|
71
|
+
last_analyzed_at TIMESTAMP WITH TIME ZONE,
|
|
72
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
73
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
-- Schema version tracking
|
|
77
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
78
|
+
version INTEGER PRIMARY KEY,
|
|
79
|
+
applied_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
-- Indexes for performance
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action);
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_projects_user_id ON projects(user_id);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_users_provider ON users(provider, provider_id);
|
|
91
|
+
`;
|
|
92
|
+
class DatabaseService {
|
|
93
|
+
pool = null;
|
|
94
|
+
initialized = false;
|
|
95
|
+
initPromise = null;
|
|
96
|
+
/**
|
|
97
|
+
* Initialize database connection pool
|
|
98
|
+
*/
|
|
99
|
+
async init() {
|
|
100
|
+
if (this.initialized)
|
|
101
|
+
return;
|
|
102
|
+
if (this.initPromise)
|
|
103
|
+
return this.initPromise;
|
|
104
|
+
this.initPromise = this._init();
|
|
105
|
+
return this.initPromise;
|
|
106
|
+
}
|
|
107
|
+
async _init() {
|
|
108
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
109
|
+
if (!databaseUrl) {
|
|
110
|
+
Logger.warn('DATABASE_URL not set - using JSON file storage');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
this.pool = new Pool({
|
|
115
|
+
connectionString: databaseUrl,
|
|
116
|
+
max: 20, // Maximum pool size
|
|
117
|
+
idleTimeoutMillis: 30000,
|
|
118
|
+
connectionTimeoutMillis: 5000,
|
|
119
|
+
});
|
|
120
|
+
// Test connection
|
|
121
|
+
const client = await this.pool.connect();
|
|
122
|
+
await client.query('SELECT NOW()');
|
|
123
|
+
client.release();
|
|
124
|
+
// Initialize schema
|
|
125
|
+
await this.initSchema();
|
|
126
|
+
this.initialized = true;
|
|
127
|
+
Logger.success('PostgreSQL connected with connection pool (max: 20)');
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
Logger.error('PostgreSQL connection failed:', error);
|
|
131
|
+
this.pool = null;
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Initialize database schema
|
|
137
|
+
*/
|
|
138
|
+
async initSchema() {
|
|
139
|
+
if (!this.pool)
|
|
140
|
+
return;
|
|
141
|
+
try {
|
|
142
|
+
// Create schema
|
|
143
|
+
await this.pool.query(SCHEMA_SQL);
|
|
144
|
+
// Check/update schema version
|
|
145
|
+
const result = await this.pool.query('SELECT version FROM schema_version ORDER BY version DESC LIMIT 1');
|
|
146
|
+
if (result.rows.length === 0) {
|
|
147
|
+
await this.pool.query('INSERT INTO schema_version (version) VALUES ($1)', [SCHEMA_VERSION]);
|
|
148
|
+
Logger.info(`Database schema initialized (v${SCHEMA_VERSION})`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const currentVersion = result.rows[0].version;
|
|
152
|
+
if (currentVersion < SCHEMA_VERSION) {
|
|
153
|
+
// Run migrations here if needed
|
|
154
|
+
await this.pool.query('INSERT INTO schema_version (version) VALUES ($1)', [SCHEMA_VERSION]);
|
|
155
|
+
Logger.info(`Database schema migrated to v${SCHEMA_VERSION}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
Logger.error('Schema initialization failed:', error);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Check if database is available
|
|
166
|
+
*/
|
|
167
|
+
isAvailable() {
|
|
168
|
+
return this.pool !== null && this.initialized;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Get a client from the pool
|
|
172
|
+
*/
|
|
173
|
+
async getClient() {
|
|
174
|
+
if (!this.pool) {
|
|
175
|
+
throw new Error('Database not initialized');
|
|
176
|
+
}
|
|
177
|
+
return this.pool.connect();
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Execute a query
|
|
181
|
+
*/
|
|
182
|
+
async query(text, params) {
|
|
183
|
+
if (!this.pool) {
|
|
184
|
+
throw new Error('Database not initialized');
|
|
185
|
+
}
|
|
186
|
+
return this.pool.query(text, params);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Execute a transaction
|
|
190
|
+
*/
|
|
191
|
+
async transaction(callback) {
|
|
192
|
+
const client = await this.getClient();
|
|
193
|
+
try {
|
|
194
|
+
await client.query('BEGIN');
|
|
195
|
+
const result = await callback(client);
|
|
196
|
+
await client.query('COMMIT');
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
await client.query('ROLLBACK');
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
client.release();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Close the connection pool
|
|
209
|
+
*/
|
|
210
|
+
async close() {
|
|
211
|
+
if (this.pool) {
|
|
212
|
+
await this.pool.end();
|
|
213
|
+
this.pool = null;
|
|
214
|
+
this.initialized = false;
|
|
215
|
+
Logger.info('Database connection pool closed');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Export singleton instance
|
|
220
|
+
export const db = new DatabaseService();
|
|
221
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption Service for ArchiCore
|
|
3
|
+
*
|
|
4
|
+
* AES-256-GCM encryption for sensitive data
|
|
5
|
+
*/
|
|
6
|
+
declare class EncryptionService {
|
|
7
|
+
private key;
|
|
8
|
+
private initialized;
|
|
9
|
+
/**
|
|
10
|
+
* Initialize encryption with key from environment
|
|
11
|
+
*/
|
|
12
|
+
init(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Encrypt a string value
|
|
15
|
+
*/
|
|
16
|
+
encrypt(plaintext: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Decrypt a string value
|
|
19
|
+
*/
|
|
20
|
+
decrypt(ciphertext: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Hash a value (one-way, for comparison)
|
|
23
|
+
*/
|
|
24
|
+
hash(value: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Check if a value is encrypted (starts with valid base64 of correct length)
|
|
27
|
+
*/
|
|
28
|
+
isEncrypted(value: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Encrypt if not already encrypted
|
|
31
|
+
*/
|
|
32
|
+
ensureEncrypted(value: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Mask sensitive data for logging (show only last 4 chars)
|
|
35
|
+
*/
|
|
36
|
+
mask(value: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Generate a secure random token
|
|
39
|
+
*/
|
|
40
|
+
generateToken(length?: number): string;
|
|
41
|
+
/**
|
|
42
|
+
* Generate a secure random secret
|
|
43
|
+
*/
|
|
44
|
+
generateSecret(length?: number): string;
|
|
45
|
+
}
|
|
46
|
+
export declare const encryption: EncryptionService;
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=encryption.d.ts.map
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption Service for ArchiCore
|
|
3
|
+
*
|
|
4
|
+
* AES-256-GCM encryption for sensitive data
|
|
5
|
+
*/
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import { Logger } from '../../utils/logger.js';
|
|
8
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
9
|
+
const IV_LENGTH = 16;
|
|
10
|
+
const AUTH_TAG_LENGTH = 16;
|
|
11
|
+
// const SALT_LENGTH = 32; // Reserved for future use
|
|
12
|
+
class EncryptionService {
|
|
13
|
+
key = null;
|
|
14
|
+
initialized = false;
|
|
15
|
+
/**
|
|
16
|
+
* Initialize encryption with key from environment
|
|
17
|
+
*/
|
|
18
|
+
init() {
|
|
19
|
+
if (this.initialized)
|
|
20
|
+
return;
|
|
21
|
+
const encryptionKey = process.env.ENCRYPTION_KEY;
|
|
22
|
+
if (!encryptionKey) {
|
|
23
|
+
Logger.warn('ENCRYPTION_KEY not set - generating temporary key (NOT FOR PRODUCTION)');
|
|
24
|
+
// Generate a temporary key for development
|
|
25
|
+
this.key = crypto.randomBytes(32);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Derive key from password using PBKDF2
|
|
29
|
+
const salt = process.env.ENCRYPTION_SALT || 'archicore-default-salt';
|
|
30
|
+
this.key = crypto.pbkdf2Sync(encryptionKey, salt, 100000, 32, 'sha256');
|
|
31
|
+
}
|
|
32
|
+
this.initialized = true;
|
|
33
|
+
Logger.info('Encryption service initialized');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Encrypt a string value
|
|
37
|
+
*/
|
|
38
|
+
encrypt(plaintext) {
|
|
39
|
+
if (!this.key) {
|
|
40
|
+
this.init();
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
// Generate random IV
|
|
44
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
45
|
+
// Create cipher
|
|
46
|
+
const cipher = crypto.createCipheriv(ALGORITHM, this.key, iv);
|
|
47
|
+
// Encrypt
|
|
48
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
|
49
|
+
encrypted += cipher.final('hex');
|
|
50
|
+
// Get auth tag
|
|
51
|
+
const authTag = cipher.getAuthTag();
|
|
52
|
+
// Combine: iv + authTag + encrypted
|
|
53
|
+
const combined = Buffer.concat([
|
|
54
|
+
iv,
|
|
55
|
+
authTag,
|
|
56
|
+
Buffer.from(encrypted, 'hex')
|
|
57
|
+
]);
|
|
58
|
+
return combined.toString('base64');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
Logger.error('Encryption failed:', error);
|
|
62
|
+
throw new Error('Encryption failed');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Decrypt a string value
|
|
67
|
+
*/
|
|
68
|
+
decrypt(ciphertext) {
|
|
69
|
+
if (!this.key) {
|
|
70
|
+
this.init();
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
// Decode from base64
|
|
74
|
+
const combined = Buffer.from(ciphertext, 'base64');
|
|
75
|
+
// Extract components
|
|
76
|
+
const iv = combined.subarray(0, IV_LENGTH);
|
|
77
|
+
const authTag = combined.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
|
|
78
|
+
const encrypted = combined.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
|
|
79
|
+
// Create decipher
|
|
80
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, this.key, iv);
|
|
81
|
+
decipher.setAuthTag(authTag);
|
|
82
|
+
// Decrypt
|
|
83
|
+
let decrypted = decipher.update(encrypted);
|
|
84
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
85
|
+
return decrypted.toString('utf8');
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
Logger.error('Decryption failed:', error);
|
|
89
|
+
throw new Error('Decryption failed');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Hash a value (one-way, for comparison)
|
|
94
|
+
*/
|
|
95
|
+
hash(value) {
|
|
96
|
+
const salt = process.env.ENCRYPTION_SALT || 'archicore-default-salt';
|
|
97
|
+
return crypto
|
|
98
|
+
.createHmac('sha256', salt)
|
|
99
|
+
.update(value)
|
|
100
|
+
.digest('hex');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if a value is encrypted (starts with valid base64 of correct length)
|
|
104
|
+
*/
|
|
105
|
+
isEncrypted(value) {
|
|
106
|
+
try {
|
|
107
|
+
const decoded = Buffer.from(value, 'base64');
|
|
108
|
+
// Minimum length: IV (16) + AuthTag (16) + at least 1 byte encrypted
|
|
109
|
+
return decoded.length > IV_LENGTH + AUTH_TAG_LENGTH;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Encrypt if not already encrypted
|
|
117
|
+
*/
|
|
118
|
+
ensureEncrypted(value) {
|
|
119
|
+
if (this.isEncrypted(value)) {
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
return this.encrypt(value);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Mask sensitive data for logging (show only last 4 chars)
|
|
126
|
+
*/
|
|
127
|
+
mask(value) {
|
|
128
|
+
if (value.length <= 4) {
|
|
129
|
+
return '****';
|
|
130
|
+
}
|
|
131
|
+
return '*'.repeat(value.length - 4) + value.slice(-4);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Generate a secure random token
|
|
135
|
+
*/
|
|
136
|
+
generateToken(length = 32) {
|
|
137
|
+
return crypto.randomBytes(length).toString('hex');
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Generate a secure random secret
|
|
141
|
+
*/
|
|
142
|
+
generateSecret(length = 64) {
|
|
143
|
+
return crypto.randomBytes(length).toString('base64url');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Export singleton instance
|
|
147
|
+
export const encryption = new EncryptionService();
|
|
148
|
+
//# sourceMappingURL=encryption.js.map
|