archicore 0.2.1 → 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 +123 -2
- package/dist/orchestrator/index.js +19 -0
- package/dist/server/index.js +97 -0
- package/dist/server/routes/admin.js +291 -1
- package/dist/server/routes/api.js +17 -2
- package/dist/server/routes/developer.js +1 -1
- package/dist/server/routes/device-auth.js +10 -1
- package/dist/server/routes/report-issue.d.ts +7 -0
- package/dist/server/routes/report-issue.js +307 -0
- package/dist/server/services/auth-service.d.ts +21 -0
- package/dist/server/services/auth-service.js +51 -5
- package/dist/server/services/encryption.d.ts +48 -0
- package/dist/server/services/encryption.js +148 -0
- package/package.json +1 -1
|
@@ -17,6 +17,27 @@ export declare class AuthService {
|
|
|
17
17
|
private createDefaultAdmin;
|
|
18
18
|
private saveUsers;
|
|
19
19
|
private saveSessions;
|
|
20
|
+
private readonly BCRYPT_ROUNDS;
|
|
21
|
+
/**
|
|
22
|
+
* Hash password with bcrypt (async)
|
|
23
|
+
*/
|
|
24
|
+
private hashPasswordAsync;
|
|
25
|
+
/**
|
|
26
|
+
* Legacy SHA256 hash for backward compatibility
|
|
27
|
+
*/
|
|
28
|
+
private hashPasswordLegacy;
|
|
29
|
+
/**
|
|
30
|
+
* Verify password against hash (supports both bcrypt and legacy SHA256)
|
|
31
|
+
*/
|
|
32
|
+
private verifyPassword;
|
|
33
|
+
/**
|
|
34
|
+
* Check if password needs rehashing (from legacy to bcrypt)
|
|
35
|
+
*/
|
|
36
|
+
private needsRehash;
|
|
37
|
+
/**
|
|
38
|
+
* Sync hash for JSON fallback (still uses legacy for simplicity)
|
|
39
|
+
* @deprecated Use hashPasswordAsync for new passwords
|
|
40
|
+
*/
|
|
20
41
|
private hashPassword;
|
|
21
42
|
private createEmptyUsage;
|
|
22
43
|
private generateToken;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
7
|
import { createHash } from 'crypto';
|
|
8
8
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
9
|
+
import bcrypt from 'bcrypt';
|
|
9
10
|
import { join } from 'path';
|
|
10
11
|
import { TIER_LIMITS } from '../../types/user.js';
|
|
11
12
|
import { db } from './database.js';
|
|
@@ -65,7 +66,7 @@ export class AuthService {
|
|
|
65
66
|
const admin = {
|
|
66
67
|
id: 'admin-' + randomUUID(),
|
|
67
68
|
email: 'admin@archicore.io',
|
|
68
|
-
username: '
|
|
69
|
+
username: 'ArchiCore Team',
|
|
69
70
|
passwordHash: this.hashPassword('admin123'),
|
|
70
71
|
tier: 'admin',
|
|
71
72
|
provider: 'email',
|
|
@@ -85,9 +86,45 @@ export class AuthService {
|
|
|
85
86
|
await writeFile(sessionsPath, JSON.stringify({ sessions: this.sessions }, null, 2));
|
|
86
87
|
}
|
|
87
88
|
// ========== HELPER METHODS ==========
|
|
88
|
-
|
|
89
|
+
BCRYPT_ROUNDS = 12;
|
|
90
|
+
/**
|
|
91
|
+
* Hash password with bcrypt (async)
|
|
92
|
+
*/
|
|
93
|
+
async hashPasswordAsync(password) {
|
|
94
|
+
return bcrypt.hash(password, this.BCRYPT_ROUNDS);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Legacy SHA256 hash for backward compatibility
|
|
98
|
+
*/
|
|
99
|
+
hashPasswordLegacy(password) {
|
|
89
100
|
return createHash('sha256').update(password + 'archicore-salt-2024').digest('hex');
|
|
90
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Verify password against hash (supports both bcrypt and legacy SHA256)
|
|
104
|
+
*/
|
|
105
|
+
async verifyPassword(password, hash) {
|
|
106
|
+
// Check if it's a bcrypt hash (starts with $2b$ or $2a$)
|
|
107
|
+
if (hash.startsWith('$2')) {
|
|
108
|
+
return bcrypt.compare(password, hash);
|
|
109
|
+
}
|
|
110
|
+
// Legacy SHA256 hash
|
|
111
|
+
return this.hashPasswordLegacy(password) === hash;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check if password needs rehashing (from legacy to bcrypt)
|
|
115
|
+
*/
|
|
116
|
+
needsRehash(hash) {
|
|
117
|
+
return !hash.startsWith('$2');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Sync hash for JSON fallback (still uses legacy for simplicity)
|
|
121
|
+
* @deprecated Use hashPasswordAsync for new passwords
|
|
122
|
+
*/
|
|
123
|
+
hashPassword(password) {
|
|
124
|
+
// For new registrations, we should use async version
|
|
125
|
+
// This is kept for backward compatibility with JSON storage
|
|
126
|
+
return this.hashPasswordLegacy(password);
|
|
127
|
+
}
|
|
91
128
|
createEmptyUsage() {
|
|
92
129
|
return {
|
|
93
130
|
requestsToday: 0,
|
|
@@ -130,7 +167,7 @@ export class AuthService {
|
|
|
130
167
|
return;
|
|
131
168
|
const adminId = 'admin-' + randomUUID();
|
|
132
169
|
await db.query(`INSERT INTO users (id, email, username, password_hash, tier, provider)
|
|
133
|
-
VALUES ($1, $2, $3, $4, $5, $6)`, [adminId, 'admin@archicore.io', '
|
|
170
|
+
VALUES ($1, $2, $3, $4, $5, $6)`, [adminId, 'admin@archicore.io', 'ArchiCore Team', this.hashPassword('admin123'), 'admin', 'email']);
|
|
134
171
|
Logger.info('Default admin user created');
|
|
135
172
|
}
|
|
136
173
|
// ========== PUBLIC API ==========
|
|
@@ -149,9 +186,10 @@ export class AuthService {
|
|
|
149
186
|
}
|
|
150
187
|
const userId = 'user-' + randomUUID();
|
|
151
188
|
const token = this.generateToken();
|
|
189
|
+
const passwordHash = await this.hashPasswordAsync(password);
|
|
152
190
|
await db.transaction(async (client) => {
|
|
153
191
|
await client.query(`INSERT INTO users (id, email, username, password_hash, tier, provider)
|
|
154
|
-
VALUES ($1, $2, $3, $4, $5, $6)`, [userId, email.toLowerCase(), username,
|
|
192
|
+
VALUES ($1, $2, $3, $4, $5, $6)`, [userId, email.toLowerCase(), username, passwordHash, 'free', 'email']);
|
|
155
193
|
await client.query(`INSERT INTO sessions (token, user_id, expires_at)
|
|
156
194
|
VALUES ($1, $2, $3)`, [token, userId, new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)]);
|
|
157
195
|
});
|
|
@@ -203,10 +241,18 @@ export class AuthService {
|
|
|
203
241
|
return { success: false, error: 'Invalid email or password' };
|
|
204
242
|
}
|
|
205
243
|
const row = result.rows[0];
|
|
206
|
-
|
|
244
|
+
// Verify password (supports both bcrypt and legacy SHA256)
|
|
245
|
+
const isValid = await this.verifyPassword(password, row.password_hash || '');
|
|
246
|
+
if (!isValid) {
|
|
207
247
|
return { success: false, error: 'Invalid email or password' };
|
|
208
248
|
}
|
|
209
249
|
const token = this.generateToken();
|
|
250
|
+
// Rehash legacy passwords to bcrypt
|
|
251
|
+
if (row.password_hash && this.needsRehash(row.password_hash)) {
|
|
252
|
+
const newHash = await this.hashPasswordAsync(password);
|
|
253
|
+
await db.query('UPDATE users SET password_hash = $1 WHERE id = $2', [newHash, row.id]);
|
|
254
|
+
Logger.info(`Upgraded password hash for user ${row.id}`);
|
|
255
|
+
}
|
|
210
256
|
await db.query('UPDATE users SET last_login_at = NOW() WHERE id = $1', [row.id]);
|
|
211
257
|
await db.query('INSERT INTO sessions (token, user_id, expires_at) VALUES ($1, $2, $3)', [token, row.id, new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)]);
|
|
212
258
|
const user = this.rowToUser(row);
|
|
@@ -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
|