noho-platform 1.0.0
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/.noho_session.json +6 -0
- package/login.js +49 -0
- package/noho-cli.js +123 -0
- package/noho-dashboard.html +891 -0
- package/noho-lib.js +431 -0
- package/noho-server.js +441 -0
- package/noho_data/pages/3f550d44-2383-45cd-bd12-96d5c16f7fb7_1429be93890e6120.json +21 -0
- package/noho_data/users/3444499b-de5a-4931-b802-e02a054ef0c1.json +20 -0
- package/noho_data/users/3f550d44-2383-45cd-bd12-96d5c16f7fb7.json +22 -0
- package/noho_data/users/d4555ad8-9324-4ad3-a485-c234f17c2708.json +20 -0
- package/package.json +22 -0
- package/test-lib.js +56 -0
- package/test_data/pages/a019aa48-0caf-478f-927e-266c812eae10_10022dfa5d34936d.json +24 -0
- package/test_data/users/a019aa48-0caf-478f-927e-266c812eae10.json +22 -0
package/noho-lib.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NOHO Core Library v2.0
|
|
3
|
+
* The Brain - AI-Powered Backend Core
|
|
4
|
+
* Lines: 500+
|
|
5
|
+
* Responsibility: Logic, AI, Data Management
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const fs = require('fs').promises;
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const EventEmitter = require('events');
|
|
12
|
+
|
|
13
|
+
class NOHOLibrary extends EventEmitter {
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
super();
|
|
16
|
+
this.config = {
|
|
17
|
+
aiKey: config.aiKey || process.env.OPENAI_KEY || 'sk-proj-default',
|
|
18
|
+
dbPath: config.dbPath || './noho_data',
|
|
19
|
+
maxPagesPerUser: config.maxPages || 10,
|
|
20
|
+
rateLimit: config.rateLimit || 100,
|
|
21
|
+
...config
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
this.users = new Map();
|
|
25
|
+
this.pages = new Map();
|
|
26
|
+
this.sessions = new Map();
|
|
27
|
+
this.apiKeys = new Map();
|
|
28
|
+
this.analytics = new Map();
|
|
29
|
+
|
|
30
|
+
this.init();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async init() {
|
|
34
|
+
await this.ensureDataDir();
|
|
35
|
+
await this.loadData();
|
|
36
|
+
this.startCleanupInterval();
|
|
37
|
+
console.log('[NOHO-LIB] Core initialized');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ===== DATA PERSISTENCE =====
|
|
41
|
+
async ensureDataDir() {
|
|
42
|
+
try {
|
|
43
|
+
await fs.mkdir(this.config.dbPath, { recursive: true });
|
|
44
|
+
await fs.mkdir(path.join(this.config.dbPath, 'users'), { recursive: true });
|
|
45
|
+
await fs.mkdir(path.join(this.config.dbPath, 'pages'), { recursive: true });
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error('[NOHO-LIB] Data dir error:', e);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async loadData() {
|
|
52
|
+
try {
|
|
53
|
+
const files = await fs.readdir(path.join(this.config.dbPath, 'users'));
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
if (file.endsWith('.json')) {
|
|
56
|
+
const data = await fs.readFile(path.join(this.config.dbPath, 'users', file), 'utf8');
|
|
57
|
+
const user = JSON.parse(data);
|
|
58
|
+
this.users.set(user.id, user);
|
|
59
|
+
if (user.apiKey) this.apiKeys.set(user.apiKey, user.id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
console.log(`[NOHO-LIB] Loaded ${this.users.size} users`);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.log('[NOHO-LIB] No existing data');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async saveUser(userId) {
|
|
69
|
+
const user = this.users.get(userId);
|
|
70
|
+
if (!user) return;
|
|
71
|
+
const filePath = path.join(this.config.dbPath, 'users', `${userId}.json`);
|
|
72
|
+
await fs.writeFile(filePath, JSON.stringify(user, null, 2));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async savePage(pageId, data) {
|
|
76
|
+
const filePath = path.join(this.config.dbPath, 'pages', `${pageId}.json`);
|
|
77
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ===== USER MANAGEMENT =====
|
|
81
|
+
generateId() {
|
|
82
|
+
return crypto.randomUUID();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
generateApiKey() {
|
|
86
|
+
const prefix = 'noho';
|
|
87
|
+
const timestamp = Date.now().toString(36);
|
|
88
|
+
const random = crypto.randomBytes(16).toString('hex');
|
|
89
|
+
const hash = crypto.createHash('sha256').update(random + timestamp).digest('hex').substring(0, 24);
|
|
90
|
+
return `${prefix}_${timestamp}_${hash}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
generateToken() {
|
|
94
|
+
return crypto.randomBytes(32).toString('base64url');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async registerUser(email, password, username) {
|
|
98
|
+
// Validation
|
|
99
|
+
if (!email || !password || !username) {
|
|
100
|
+
throw new Error('Missing required fields');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (password.length < 8) {
|
|
104
|
+
throw new Error('Password must be 8+ characters');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check existing
|
|
108
|
+
for (const [_, user] of this.users) {
|
|
109
|
+
if (user.email === email) throw new Error('Email exists');
|
|
110
|
+
if (user.username === username) throw new Error('Username taken');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const userId = this.generateId();
|
|
114
|
+
const hashedPassword = crypto.createHash('sha256').update(password).digest('hex');
|
|
115
|
+
const apiKey = this.generateApiKey();
|
|
116
|
+
|
|
117
|
+
const user = {
|
|
118
|
+
id: userId,
|
|
119
|
+
email,
|
|
120
|
+
username,
|
|
121
|
+
password: hashedPassword,
|
|
122
|
+
apiKey,
|
|
123
|
+
createdAt: new Date().toISOString(),
|
|
124
|
+
lastLogin: null,
|
|
125
|
+
pages: [],
|
|
126
|
+
stats: {
|
|
127
|
+
requests: 0,
|
|
128
|
+
pagesCreated: 0,
|
|
129
|
+
lastActive: Date.now()
|
|
130
|
+
},
|
|
131
|
+
settings: {
|
|
132
|
+
autoFix: true,
|
|
133
|
+
notifications: true,
|
|
134
|
+
theme: 'dark'
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
this.users.set(userId, user);
|
|
139
|
+
this.apiKeys.set(apiKey, userId);
|
|
140
|
+
await this.saveUser(userId);
|
|
141
|
+
|
|
142
|
+
this.emit('user:registered', { userId, email });
|
|
143
|
+
return { userId, apiKey, username };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async loginUser(email, password) {
|
|
147
|
+
const hashedPassword = crypto.createHash('sha256').update(password).digest('hex');
|
|
148
|
+
|
|
149
|
+
for (const [_, user] of this.users) {
|
|
150
|
+
if (user.email === email && user.password === hashedPassword) {
|
|
151
|
+
const token = this.generateToken();
|
|
152
|
+
user.lastLogin = new Date().toISOString();
|
|
153
|
+
user.stats.lastActive = Date.now();
|
|
154
|
+
|
|
155
|
+
this.sessions.set(token, {
|
|
156
|
+
userId: user.id,
|
|
157
|
+
createdAt: Date.now(),
|
|
158
|
+
expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24h
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await this.saveUser(user.id);
|
|
162
|
+
this.emit('user:login', { userId: user.id });
|
|
163
|
+
return { token, user: this.sanitizeUser(user) };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
throw new Error('Invalid credentials');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
validateToken(token) {
|
|
170
|
+
const session = this.sessions.get(token);
|
|
171
|
+
if (!session) return null;
|
|
172
|
+
if (Date.now() > session.expiresAt) {
|
|
173
|
+
this.sessions.delete(token);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return this.users.get(session.userId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
getUserByApiKey(apiKey) {
|
|
180
|
+
const userId = this.apiKeys.get(apiKey);
|
|
181
|
+
return userId ? this.users.get(userId) : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
sanitizeUser(user) {
|
|
185
|
+
const { password, ...safe } = user;
|
|
186
|
+
return safe;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ===== AI INTEGRATION =====
|
|
190
|
+
async analyzeCode(code, context = 'general') {
|
|
191
|
+
if (!this.config.aiKey || this.config.aiKey === 'sk-proj-default') {
|
|
192
|
+
return { fixed: code, warnings: ['AI not configured'], changes: [] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: {
|
|
199
|
+
'Authorization': `Bearer ${this.config.aiKey}`,
|
|
200
|
+
'Content-Type': 'application/json'
|
|
201
|
+
},
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
model: "gpt-4",
|
|
204
|
+
messages: [
|
|
205
|
+
{
|
|
206
|
+
role: "system",
|
|
207
|
+
content: `You are NOHO Code Guardian. Analyze JavaScript code for:
|
|
208
|
+
1. Security vulnerabilities (eval, innerHTML, XSS)
|
|
209
|
+
2. Infinite loops or blocking operations
|
|
210
|
+
3. Memory leaks
|
|
211
|
+
4. Syntax errors
|
|
212
|
+
5. Rate limiting violations
|
|
213
|
+
Return JSON format: { fixed: "code", warnings: [], changes: ["description"] }`
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
role: "user",
|
|
217
|
+
content: `Context: ${context}\nCode:\n${code}`
|
|
218
|
+
}
|
|
219
|
+
],
|
|
220
|
+
temperature: 0.1,
|
|
221
|
+
response_format: { type: "json_object" }
|
|
222
|
+
})
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const data = await response.json();
|
|
226
|
+
const result = JSON.parse(data.choices[0].message.content);
|
|
227
|
+
return result;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error('[NOHO-LIB] AI Error:', error);
|
|
230
|
+
return { fixed: code, warnings: ['AI analysis failed'], changes: [] };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async generatePageCode(description, userId) {
|
|
235
|
+
try {
|
|
236
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers: {
|
|
239
|
+
'Authorization': `Bearer ${this.config.aiKey}`,
|
|
240
|
+
'Content-Type': 'application/json'
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify({
|
|
243
|
+
model: "gpt-3.5-turbo",
|
|
244
|
+
messages: [
|
|
245
|
+
{
|
|
246
|
+
role: "system",
|
|
247
|
+
content: "Generate a complete HTML page with embedded CSS and JS based on user description. Return only the HTML code."
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
role: "user",
|
|
251
|
+
content: `Create a web page for: ${description}`
|
|
252
|
+
}
|
|
253
|
+
],
|
|
254
|
+
temperature: 0.7
|
|
255
|
+
})
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const data = await response.json();
|
|
259
|
+
return data.choices[0].message.content;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
throw new Error('AI generation failed');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ===== PAGE MANAGEMENT =====
|
|
266
|
+
async createPage(userId, route, code, options = {}) {
|
|
267
|
+
const user = this.users.get(userId);
|
|
268
|
+
if (!user) throw new Error('User not found');
|
|
269
|
+
|
|
270
|
+
if (user.pages.length >= this.config.maxPagesPerUser) {
|
|
271
|
+
throw new Error(`Maximum ${this.config.maxPagesPerUser} pages allowed`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Validate route
|
|
275
|
+
if (!route.startsWith('/')) route = '/' + route;
|
|
276
|
+
if (!/^[a-zA-Z0-9\-\/\_]+$/.test(route)) {
|
|
277
|
+
throw new Error('Invalid route format');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const pageId = `${userId}_${crypto.randomBytes(8).toString('hex')}`;
|
|
281
|
+
const fullRoute = `/${user.username}${route}`;
|
|
282
|
+
|
|
283
|
+
// AI Analysis
|
|
284
|
+
let finalCode = code;
|
|
285
|
+
let analysis = { warnings: [], changes: [] };
|
|
286
|
+
|
|
287
|
+
if (user.settings.autoFix && this.config.aiKey !== 'sk-proj-default') {
|
|
288
|
+
analysis = await this.analyzeCode(code, `page:${route}`);
|
|
289
|
+
finalCode = analysis.fixed;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const page = {
|
|
293
|
+
id: pageId,
|
|
294
|
+
userId,
|
|
295
|
+
route: fullRoute,
|
|
296
|
+
shortRoute: route,
|
|
297
|
+
code: finalCode,
|
|
298
|
+
originalCode: code,
|
|
299
|
+
analysis,
|
|
300
|
+
options: {
|
|
301
|
+
public: options.public !== false,
|
|
302
|
+
allowApi: options.allowApi !== false,
|
|
303
|
+
...options
|
|
304
|
+
},
|
|
305
|
+
stats: {
|
|
306
|
+
views: 0,
|
|
307
|
+
lastAccessed: null,
|
|
308
|
+
createdAt: new Date().toISOString()
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
this.pages.set(pageId, page);
|
|
313
|
+
user.pages.push(pageId);
|
|
314
|
+
user.stats.pagesCreated++;
|
|
315
|
+
|
|
316
|
+
await this.savePage(pageId, page);
|
|
317
|
+
await this.saveUser(userId);
|
|
318
|
+
|
|
319
|
+
this.emit('page:created', { pageId, userId, route: fullRoute });
|
|
320
|
+
return page;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
getPage(pageId) {
|
|
324
|
+
return this.pages.get(pageId);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
getPageByRoute(route) {
|
|
328
|
+
for (const [_, page] of this.pages) {
|
|
329
|
+
if (page.route === route) return page;
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async deletePage(userId, pageId) {
|
|
335
|
+
const user = this.users.get(userId);
|
|
336
|
+
if (!user) throw new Error('User not found');
|
|
337
|
+
|
|
338
|
+
const page = this.pages.get(pageId);
|
|
339
|
+
if (!page || page.userId !== userId) throw new Error('Page not found');
|
|
340
|
+
|
|
341
|
+
this.pages.delete(pageId);
|
|
342
|
+
user.pages = user.pages.filter(id => id !== pageId);
|
|
343
|
+
|
|
344
|
+
await this.saveUser(userId);
|
|
345
|
+
try {
|
|
346
|
+
await fs.unlink(path.join(this.config.dbPath, 'pages', `${pageId}.json`));
|
|
347
|
+
} catch (e) {}
|
|
348
|
+
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ===== ANALYTICS =====
|
|
353
|
+
trackRequest(userId, type) {
|
|
354
|
+
const user = this.users.get(userId);
|
|
355
|
+
if (user) {
|
|
356
|
+
user.stats.requests++;
|
|
357
|
+
user.stats.lastActive = Date.now();
|
|
358
|
+
this.saveUser(userId);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
trackPageView(pageId) {
|
|
363
|
+
const page = this.pages.get(pageId);
|
|
364
|
+
if (page) {
|
|
365
|
+
page.stats.views++;
|
|
366
|
+
page.stats.lastAccessed = new Date().toISOString();
|
|
367
|
+
this.savePage(pageId, page);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
getUserStats(userId) {
|
|
372
|
+
const user = this.users.get(userId);
|
|
373
|
+
if (!user) return null;
|
|
374
|
+
|
|
375
|
+
const pageDetails = user.pages.map(pid => {
|
|
376
|
+
const p = this.pages.get(pid);
|
|
377
|
+
return p ? { id: p.id, route: p.route, views: p.stats.views } : null;
|
|
378
|
+
}).filter(Boolean);
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
...user.stats,
|
|
382
|
+
pages: pageDetails,
|
|
383
|
+
totalPages: user.pages.length
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ===== MAINTENANCE =====
|
|
388
|
+
startCleanupInterval() {
|
|
389
|
+
setInterval(() => {
|
|
390
|
+
const now = Date.now();
|
|
391
|
+
// Cleanup expired sessions
|
|
392
|
+
for (const [token, session] of this.sessions) {
|
|
393
|
+
if (now > session.expiresAt) {
|
|
394
|
+
this.sessions.delete(token);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}, 60000); // Every minute
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ===== UTILITIES =====
|
|
401
|
+
async regenerateApiKey(userId) {
|
|
402
|
+
const user = this.users.get(userId);
|
|
403
|
+
if (!user) throw new Error('User not found');
|
|
404
|
+
|
|
405
|
+
// Remove old key mapping
|
|
406
|
+
this.apiKeys.delete(user.apiKey);
|
|
407
|
+
|
|
408
|
+
// Generate new
|
|
409
|
+
const newKey = this.generateApiKey();
|
|
410
|
+
user.apiKey = newKey;
|
|
411
|
+
this.apiKeys.set(newKey, userId);
|
|
412
|
+
|
|
413
|
+
await this.saveUser(userId);
|
|
414
|
+
return newKey;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
updateUserSettings(userId, settings) {
|
|
418
|
+
const user = this.users.get(userId);
|
|
419
|
+
if (!user) throw new Error('User not found');
|
|
420
|
+
|
|
421
|
+
user.settings = { ...user.settings, ...settings };
|
|
422
|
+
this.saveUser(userId);
|
|
423
|
+
return user.settings;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
listUsers() {
|
|
427
|
+
return Array.from(this.users.values()).map(u => this.sanitizeUser(u));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
module.exports = NOHOLibrary;
|