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-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;