agentdev-webui 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.
Files changed (39) hide show
  1. package/lib/agent-api.js +530 -0
  2. package/lib/auth.js +127 -0
  3. package/lib/config.js +53 -0
  4. package/lib/database.js +762 -0
  5. package/lib/device-flow.js +257 -0
  6. package/lib/email.js +420 -0
  7. package/lib/encryption.js +112 -0
  8. package/lib/github.js +339 -0
  9. package/lib/history.js +143 -0
  10. package/lib/pwa.js +107 -0
  11. package/lib/redis-logs.js +226 -0
  12. package/lib/routes.js +680 -0
  13. package/migrations/000_create_database.sql +33 -0
  14. package/migrations/001_create_agentdev_schema.sql +135 -0
  15. package/migrations/001_create_agentdev_schema.sql.old +100 -0
  16. package/migrations/001_create_agentdev_schema_fixed.sql +135 -0
  17. package/migrations/002_add_github_token.sql +17 -0
  18. package/migrations/003_add_agent_logs_table.sql +23 -0
  19. package/migrations/004_remove_oauth_columns.sql +11 -0
  20. package/migrations/005_add_projects.sql +44 -0
  21. package/migrations/006_project_github_token.sql +7 -0
  22. package/migrations/007_project_repositories.sql +12 -0
  23. package/migrations/008_add_notifications.sql +20 -0
  24. package/migrations/009_unified_oauth.sql +153 -0
  25. package/migrations/README.md +97 -0
  26. package/package.json +37 -0
  27. package/public/css/styles.css +1140 -0
  28. package/public/device.html +384 -0
  29. package/public/docs.html +862 -0
  30. package/public/docs.md +697 -0
  31. package/public/favicon.svg +5 -0
  32. package/public/index.html +271 -0
  33. package/public/js/app.js +2379 -0
  34. package/public/login.html +224 -0
  35. package/public/profile.html +394 -0
  36. package/public/register.html +392 -0
  37. package/public/reset-password.html +349 -0
  38. package/public/verify-email.html +177 -0
  39. package/server.js +1450 -0
@@ -0,0 +1,530 @@
1
+ const { EventEmitter } = require('events');
2
+ const deviceFlow = require('./device-flow');
3
+ const db = require('./database');
4
+ const redisLogs = require('./redis-logs');
5
+ const encryption = require('./encryption');
6
+
7
+ // Event emitter for notifying server.js about completions (SSE broadcast)
8
+ const events = new EventEmitter();
9
+
10
+ /**
11
+ * Middleware to verify agent JWT token
12
+ */
13
+ async function verifyAgentAuth(req) {
14
+ const authHeader = req.headers.authorization;
15
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
16
+ return { error: 'Missing or invalid Authorization header', status: 401 };
17
+ }
18
+
19
+ const token = authHeader.substring(7);
20
+ const decoded = deviceFlow.verifyAgentToken(token);
21
+
22
+ if (!decoded) {
23
+ return { error: 'Invalid or expired token', status: 401 };
24
+ }
25
+
26
+ // Verify agent exists
27
+ const agent = await db.getAgentById(decoded.agent_id);
28
+ if (!agent) {
29
+ return { error: 'Agent not found', status: 404 };
30
+ }
31
+
32
+ // Verify token hash matches (prevent token reuse after regeneration)
33
+ const tokenHash = deviceFlow.hashToken(token);
34
+ if (agent.access_token_hash !== tokenHash) {
35
+ return { error: 'Token has been revoked', status: 401 };
36
+ }
37
+
38
+ return { agent, user: { id: decoded.user_id } };
39
+ }
40
+
41
+ /**
42
+ * POST /api/agent/device/code
43
+ * Request device authorization code
44
+ */
45
+ async function handleDeviceCodeRequest(req, res) {
46
+ let body = '';
47
+ req.on('data', chunk => body += chunk);
48
+ req.on('end', async () => {
49
+ try {
50
+ const { agent_name, capabilities } = JSON.parse(body);
51
+
52
+ if (!agent_name) {
53
+ res.writeHead(400, { 'Content-Type': 'application/json' });
54
+ res.end(JSON.stringify({ error: 'agent_name is required' }));
55
+ return;
56
+ }
57
+
58
+ const result = await deviceFlow.requestDeviceCode(agent_name, capabilities || {});
59
+
60
+ res.writeHead(200, { 'Content-Type': 'application/json' });
61
+ res.end(JSON.stringify(result));
62
+ } catch (error) {
63
+ console.error('Device code request error:', error);
64
+ res.writeHead(500, { 'Content-Type': 'application/json' });
65
+ res.end(JSON.stringify({ error: error.message }));
66
+ }
67
+ });
68
+ }
69
+
70
+ /**
71
+ * POST /api/agent/device/token
72
+ * Poll for device authorization token
73
+ */
74
+ async function handleDeviceTokenPoll(req, res) {
75
+ let body = '';
76
+ req.on('data', chunk => body += chunk);
77
+ req.on('end', async () => {
78
+ try {
79
+ const { device_code } = JSON.parse(body);
80
+
81
+ if (!device_code) {
82
+ res.writeHead(400, { 'Content-Type': 'application/json' });
83
+ res.end(JSON.stringify({ error: 'device_code is required' }));
84
+ return;
85
+ }
86
+
87
+ const result = await deviceFlow.pollDeviceToken(device_code);
88
+
89
+ if (result.error) {
90
+ const statusCode = result.error === 'authorization_pending' ? 428 : 400;
91
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
92
+ res.end(JSON.stringify(result));
93
+ } else {
94
+ res.writeHead(200, { 'Content-Type': 'application/json' });
95
+ res.end(JSON.stringify(result));
96
+ }
97
+ } catch (error) {
98
+ console.error('Device token poll error:', error);
99
+ res.writeHead(500, { 'Content-Type': 'application/json' });
100
+ res.end(JSON.stringify({ error: error.message }));
101
+ }
102
+ });
103
+ }
104
+
105
+ /**
106
+ * POST /api/agent/heartbeat
107
+ * Agent heartbeat to update status
108
+ */
109
+ async function handleHeartbeat(req, res) {
110
+ // Verify token but allow missing agent (will auto-create)
111
+ const authHeader = req.headers.authorization;
112
+
113
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
114
+ res.writeHead(401, { 'Content-Type': 'application/json' });
115
+ res.end(JSON.stringify({ error: 'Missing or invalid Authorization header' }));
116
+ return;
117
+ }
118
+
119
+ const token = authHeader.substring(7);
120
+ const decoded = deviceFlow.verifyAgentToken(token);
121
+
122
+ if (!decoded) {
123
+ res.writeHead(401, { 'Content-Type': 'application/json' });
124
+ res.end(JSON.stringify({ error: 'Invalid or expired token' }));
125
+ return;
126
+ }
127
+
128
+ let body = '';
129
+ req.on('data', chunk => body += chunk);
130
+ req.on('end', async () => {
131
+ try {
132
+ const { status, current_ticket, instance } = JSON.parse(body);
133
+
134
+ // Support multi-agent: instance suffix creates separate agent rows
135
+ const effectiveId = instance
136
+ ? `${decoded.agent_id}-${instance}`
137
+ : decoded.agent_id;
138
+
139
+ // Check if agent exists, if not create it
140
+ const tokenHash = deviceFlow.hashToken(token);
141
+ let agent = await db.getAgentById(effectiveId);
142
+ if (!agent) {
143
+ // For instances, inherit project_id from the parent agent
144
+ let projectId = decoded.project_id || null;
145
+ if (instance && !projectId) {
146
+ const parentAgent = await db.getAgentById(decoded.agent_id);
147
+ if (parentAgent) projectId = parentAgent.project_id;
148
+ }
149
+
150
+ // Auto-create agent (or sub-instance) from token info
151
+ await db.createAgent({
152
+ id: effectiveId,
153
+ userId: decoded.user_id,
154
+ name: effectiveId,
155
+ hostname: null,
156
+ capabilities: {},
157
+ accessTokenHash: tokenHash,
158
+ projectId
159
+ });
160
+ } else if (agent.access_token_hash !== tokenHash) {
161
+ // Sync token hash (fixes auth for other endpoints after token regeneration)
162
+ await db.updateAgentTokenHash(effectiveId, tokenHash);
163
+ }
164
+
165
+ // Update agent heartbeat
166
+ await db.updateAgentHeartbeat(
167
+ effectiveId,
168
+ status || 'idle',
169
+ current_ticket || null
170
+ );
171
+
172
+ res.writeHead(200, { 'Content-Type': 'application/json' });
173
+ res.end(JSON.stringify({ success: true }));
174
+ } catch (error) {
175
+ console.error('Heartbeat error:', error);
176
+ res.writeHead(500, { 'Content-Type': 'application/json' });
177
+ res.end(JSON.stringify({ error: error.message }));
178
+ }
179
+ });
180
+ }
181
+
182
+ /**
183
+ * GET /api/agent/work
184
+ * Claim next available ticket
185
+ */
186
+ async function handleWorkRequest(req, res) {
187
+ const auth = await verifyAgentAuth(req);
188
+ if (auth.error) {
189
+ res.writeHead(auth.status, { 'Content-Type': 'application/json' });
190
+ res.end(JSON.stringify({ error: auth.error }));
191
+ return;
192
+ }
193
+
194
+ try {
195
+ // Support multi-agent: use instance ID if provided
196
+ const urlObj = new URL(req.url, `http://${req.headers.host}`);
197
+ const instance = urlObj.searchParams.get('instance');
198
+ const decoded = deviceFlow.verifyAgentToken(
199
+ req.headers.authorization.substring(7)
200
+ );
201
+ const effectiveId = instance
202
+ ? `${decoded.agent_id}-${instance}`
203
+ : auth.agent.id;
204
+
205
+ // Get next available ticket scoped to agent's project
206
+ const ticket = await db.getAvailableTicket(auth.agent.project_id);
207
+
208
+ if (!ticket) {
209
+ res.writeHead(200, { 'Content-Type': 'application/json' });
210
+ res.end(JSON.stringify({ ticket: null }));
211
+ return;
212
+ }
213
+
214
+ // Assign ticket to agent (atomic operation)
215
+ const assigned = await db.assignTicket(ticket.id, effectiveId);
216
+
217
+ if (!assigned) {
218
+ // Another agent claimed it first
219
+ res.writeHead(200, { 'Content-Type': 'application/json' });
220
+ res.end(JSON.stringify({ ticket: null }));
221
+ return;
222
+ }
223
+
224
+ // Update agent status to busy
225
+ await db.updateAgentHeartbeat(effectiveId, 'busy', ticket.id);
226
+
227
+ res.writeHead(200, { 'Content-Type': 'application/json' });
228
+ res.end(JSON.stringify({ ticket: assigned }));
229
+ } catch (error) {
230
+ console.error('Work request error:', error);
231
+ res.writeHead(500, { 'Content-Type': 'application/json' });
232
+ res.end(JSON.stringify({ error: error.message }));
233
+ }
234
+ }
235
+
236
+ /**
237
+ * POST /api/agent/logs
238
+ * Stream log batch from agent
239
+ */
240
+ async function handleLogUpload(req, res) {
241
+ const auth = await verifyAgentAuth(req);
242
+ if (auth.error) {
243
+ res.writeHead(auth.status, { 'Content-Type': 'application/json' });
244
+ res.end(JSON.stringify({ error: auth.error }));
245
+ return;
246
+ }
247
+
248
+ let body = '';
249
+ req.on('data', chunk => body += chunk);
250
+ req.on('end', async () => {
251
+ try {
252
+ const { logs, ticket_id, instance } = JSON.parse(body);
253
+
254
+ if (!Array.isArray(logs)) {
255
+ res.writeHead(400, { 'Content-Type': 'application/json' });
256
+ res.end(JSON.stringify({ error: 'logs must be an array' }));
257
+ return;
258
+ }
259
+
260
+ // Support multi-agent: use instance ID if provided
261
+ const decoded = deviceFlow.verifyAgentToken(
262
+ req.headers.authorization.substring(7)
263
+ );
264
+ const effectiveId = instance
265
+ ? `${decoded.agent_id}-${instance}`
266
+ : auth.agent.id;
267
+
268
+ // Process each log entry
269
+ for (const log of logs) {
270
+ const content = typeof log === 'string' ? log : log.content;
271
+ const level = typeof log === 'object' ? log.level : 'INFO';
272
+
273
+ // Store in database
274
+ await db.insertLog(effectiveId, ticket_id, content, level);
275
+
276
+ // Publish to Redis for real-time streaming
277
+ await redisLogs.publishLog(effectiveId, {
278
+ content,
279
+ level,
280
+ timestamp: new Date().toISOString()
281
+ });
282
+ }
283
+
284
+ res.writeHead(200, { 'Content-Type': 'application/json' });
285
+ res.end(JSON.stringify({ success: true, count: logs.length }));
286
+ } catch (error) {
287
+ console.error('Log upload error:', error);
288
+ res.writeHead(500, { 'Content-Type': 'application/json' });
289
+ res.end(JSON.stringify({ error: error.message }));
290
+ }
291
+ });
292
+ }
293
+
294
+ /**
295
+ * POST /api/agent/complete
296
+ * Mark ticket as complete
297
+ */
298
+ async function handleWorkComplete(req, res, onComplete) {
299
+ const auth = await verifyAgentAuth(req);
300
+ if (auth.error) {
301
+ res.writeHead(auth.status, { 'Content-Type': 'application/json' });
302
+ res.end(JSON.stringify({ error: auth.error }));
303
+ return;
304
+ }
305
+
306
+ let body = '';
307
+ req.on('data', chunk => body += chunk);
308
+ req.on('end', async () => {
309
+ try {
310
+ const { ticket_id, success, error_message, instance } = JSON.parse(body);
311
+
312
+ if (!ticket_id) {
313
+ res.writeHead(400, { 'Content-Type': 'application/json' });
314
+ res.end(JSON.stringify({ error: 'ticket_id is required' }));
315
+ return;
316
+ }
317
+
318
+ // Support multi-agent: use instance ID if provided
319
+ const decoded = deviceFlow.verifyAgentToken(
320
+ req.headers.authorization.substring(7)
321
+ );
322
+ const effectiveId = instance
323
+ ? `${decoded.agent_id}-${instance}`
324
+ : auth.agent.id;
325
+
326
+ // Update ticket status
327
+ const status = success ? 'completed' : 'failed';
328
+ const updatedTicket = await db.updateTicketStatus(ticket_id, status, error_message);
329
+ const issueNum = updatedTicket?.github_issue_number || ticket_id;
330
+
331
+ // Set agent back to idle
332
+ await db.updateAgentHeartbeat(effectiveId, 'idle', null);
333
+
334
+ // Persist notification in DB
335
+ try {
336
+ const notifTitle = success
337
+ ? `Ticket #${issueNum} completed`
338
+ : `Ticket #${issueNum} failed`;
339
+ await db.createNotification(
340
+ auth.user.id,
341
+ 'ticket-completed',
342
+ notifTitle,
343
+ `Agent ${effectiveId} ${success ? 'completed' : 'failed'} ticket #${issueNum}`,
344
+ { ticket_id, agent_id: effectiveId, success }
345
+ );
346
+ } catch (err) {
347
+ console.error('Failed to persist notification:', err.message);
348
+ }
349
+
350
+ // Notify frontend of completion via SSE
351
+ const completionData = {
352
+ id: effectiveId,
353
+ ticket: issueNum,
354
+ title: effectiveId,
355
+ success,
356
+ status,
357
+ user_id: auth.user.id
358
+ };
359
+ events.emit('ticket-completed', completionData);
360
+ if (onComplete) onComplete(completionData);
361
+
362
+ res.writeHead(200, { 'Content-Type': 'application/json' });
363
+ res.end(JSON.stringify({ success: true }));
364
+ } catch (error) {
365
+ console.error('Work complete error:', error);
366
+ res.writeHead(500, { 'Content-Type': 'application/json' });
367
+ res.end(JSON.stringify({ error: error.message }));
368
+ }
369
+ });
370
+ }
371
+
372
+ /**
373
+ * GET /api/agent/oauth
374
+ * Get user's GitHub token for agent use
375
+ */
376
+ async function handleOAuthRequest(req, res) {
377
+ const auth = await verifyAgentAuth(req);
378
+ if (auth.error) {
379
+ res.writeHead(auth.status, { 'Content-Type': 'application/json' });
380
+ res.end(JSON.stringify({ error: auth.error }));
381
+ return;
382
+ }
383
+
384
+ try {
385
+ // Look up GitHub OAuth provider from unified table
386
+ const oauthProvider = await db.getOAuthProvider(auth.user.id, 'github');
387
+
388
+ if (!oauthProvider || !oauthProvider.access_token) {
389
+ // Fallback: check legacy github_token_encrypted column
390
+ const user = await db.getUserById(auth.user.id);
391
+ if (user && user.github_token_encrypted) {
392
+ const githubToken = encryption.decrypt(user.github_token_encrypted);
393
+ res.writeHead(200, { 'Content-Type': 'application/json' });
394
+ res.end(JSON.stringify({
395
+ github: { token: githubToken }
396
+ }));
397
+ return;
398
+ }
399
+
400
+ res.writeHead(404, { 'Content-Type': 'application/json' });
401
+ res.end(JSON.stringify({ error: 'GitHub token not configured. Add it in your profile.' }));
402
+ return;
403
+ }
404
+
405
+ // Decrypt token from unified OAuth table
406
+ const githubToken = encryption.decrypt(oauthProvider.access_token);
407
+
408
+ res.writeHead(200, { 'Content-Type': 'application/json' });
409
+ res.end(JSON.stringify({
410
+ github: {
411
+ token: githubToken,
412
+ provider_key: oauthProvider.provider_key
413
+ }
414
+ }));
415
+ } catch (error) {
416
+ console.error('OAuth request error:', error);
417
+ res.writeHead(500, { 'Content-Type': 'application/json' });
418
+ res.end(JSON.stringify({ error: error.message }));
419
+ }
420
+ }
421
+
422
+ /**
423
+ * POST /api/agent/ensure-ticket
424
+ * Upsert a ticket by github_repo + github_issue_number, return real DB id.
425
+ * Used by agents resuming leftover tickets found on the project board.
426
+ */
427
+ async function handleEnsureTicket(req, res) {
428
+ const auth = await verifyAgentAuth(req);
429
+ if (auth.error) {
430
+ res.writeHead(auth.status, { 'Content-Type': 'application/json' });
431
+ res.end(JSON.stringify({ error: auth.error }));
432
+ return;
433
+ }
434
+
435
+ let body = '';
436
+ req.on('data', chunk => body += chunk);
437
+ req.on('end', async () => {
438
+ try {
439
+ const { github_repo, github_issue_number, github_project_item_id, instance } = JSON.parse(body);
440
+
441
+ if (!github_repo || !github_issue_number) {
442
+ res.writeHead(400, { 'Content-Type': 'application/json' });
443
+ res.end(JSON.stringify({ error: 'github_repo and github_issue_number are required' }));
444
+ return;
445
+ }
446
+
447
+ // Support multi-agent: use instance ID if provided
448
+ const decoded = deviceFlow.verifyAgentToken(
449
+ req.headers.authorization.substring(7)
450
+ );
451
+ const effectiveId = instance
452
+ ? `${decoded.agent_id}-${instance}`
453
+ : auth.agent.id;
454
+
455
+ const ticket = await db.createTicket({
456
+ issueNumber: github_issue_number,
457
+ repo: github_repo,
458
+ projectItemId: github_project_item_id || null,
459
+ priority: 5,
460
+ projectId: auth.agent.project_id || null
461
+ });
462
+
463
+ // Try to claim atomically — returns null if another agent already has it
464
+ let assigned = await db.assignTicket(ticket.id, effectiveId);
465
+
466
+ if (!assigned) {
467
+ // If the assigned agent is stale (no heartbeat in 2 min), reclaim
468
+ assigned = await db.reassignStaleTicket(ticket.id, effectiveId, 2);
469
+ if (!assigned) {
470
+ res.writeHead(200, { 'Content-Type': 'application/json' });
471
+ res.end(JSON.stringify({ ticket, claimed: false }));
472
+ return;
473
+ }
474
+ }
475
+
476
+ res.writeHead(200, { 'Content-Type': 'application/json' });
477
+ res.end(JSON.stringify({ ticket: assigned, claimed: true }));
478
+ } catch (error) {
479
+ console.error('Ensure ticket error:', error);
480
+ res.writeHead(500, { 'Content-Type': 'application/json' });
481
+ res.end(JSON.stringify({ error: error.message }));
482
+ }
483
+ });
484
+ }
485
+
486
+ /**
487
+ * GET /api/agent/should-stop?ticket_id=X
488
+ * Check if a stop was requested for a ticket
489
+ */
490
+ async function handleShouldStop(req, res) {
491
+ const auth = await verifyAgentAuth(req);
492
+ if (auth.error) {
493
+ res.writeHead(auth.status, { 'Content-Type': 'application/json' });
494
+ res.end(JSON.stringify({ error: auth.error }));
495
+ return;
496
+ }
497
+
498
+ const urlObj = new URL(req.url, `http://${req.headers.host}`);
499
+ const ticketId = urlObj.searchParams.get('ticket_id');
500
+
501
+ if (!ticketId) {
502
+ res.writeHead(400, { 'Content-Type': 'application/json' });
503
+ res.end(JSON.stringify({ error: 'ticket_id is required' }));
504
+ return;
505
+ }
506
+
507
+ try {
508
+ const stopSignal = await redisLogs.get(`agentdev:stop:${ticketId}`);
509
+ res.writeHead(200, { 'Content-Type': 'application/json' });
510
+ res.end(JSON.stringify({ should_stop: !!stopSignal }));
511
+ } catch (error) {
512
+ console.error('Should-stop check error:', error);
513
+ res.writeHead(500, { 'Content-Type': 'application/json' });
514
+ res.end(JSON.stringify({ error: error.message }));
515
+ }
516
+ }
517
+
518
+ module.exports = {
519
+ events,
520
+ verifyAgentAuth,
521
+ handleDeviceCodeRequest,
522
+ handleDeviceTokenPoll,
523
+ handleHeartbeat,
524
+ handleWorkRequest,
525
+ handleLogUpload,
526
+ handleWorkComplete,
527
+ handleOAuthRequest,
528
+ handleShouldStop,
529
+ handleEnsureTicket
530
+ };
package/lib/auth.js ADDED
@@ -0,0 +1,127 @@
1
+ const crypto = require('crypto');
2
+ const config = require('./config');
3
+ const db = require('./database');
4
+
5
+ // In-memory session store
6
+ const sessions = new Map();
7
+
8
+ function generateSessionId() {
9
+ return crypto.randomBytes(32).toString('hex');
10
+ }
11
+
12
+ function hashPassword(password) {
13
+ return crypto.createHash('sha256').update(password).digest('hex');
14
+ }
15
+
16
+ function createSession(userId, email) {
17
+ const sessionId = generateSessionId();
18
+ const session = {
19
+ userId,
20
+ email,
21
+ createdAt: Date.now(),
22
+ expiresAt: Date.now() + config.AUTH.SESSION_TTL
23
+ };
24
+ sessions.set(sessionId, session);
25
+ return sessionId;
26
+ }
27
+
28
+ function validateSession(sessionId) {
29
+ if (!sessionId) return false;
30
+ const session = sessions.get(sessionId);
31
+ if (!session) return false;
32
+ if (Date.now() > session.expiresAt) {
33
+ sessions.delete(sessionId);
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+
39
+ function getSession(sessionId) {
40
+ return sessions.get(sessionId);
41
+ }
42
+
43
+ function destroySession(sessionId) {
44
+ sessions.delete(sessionId);
45
+ }
46
+
47
+ async function validateCredentials(email, password) {
48
+ try {
49
+ const passwordHash = hashPassword(password);
50
+ const user = await db.getUserByEmail(email);
51
+
52
+ if (!user) {
53
+ return null;
54
+ }
55
+
56
+ if (user.password_hash === passwordHash) {
57
+ return user;
58
+ }
59
+
60
+ return null;
61
+ } catch (error) {
62
+ console.error('Error validating credentials:', error);
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function parseCookies(cookieHeader) {
68
+ const cookies = {};
69
+ if (!cookieHeader) return cookies;
70
+ cookieHeader.split(';').forEach(cookie => {
71
+ const [name, ...rest] = cookie.trim().split('=');
72
+ if (name && rest.length) {
73
+ cookies[name] = rest.join('=');
74
+ }
75
+ });
76
+ return cookies;
77
+ }
78
+
79
+ function getSessionFromRequest(req) {
80
+ const cookies = parseCookies(req.headers.cookie);
81
+ return cookies.session;
82
+ }
83
+
84
+ function isAuthenticated(req) {
85
+ const sessionId = getSessionFromRequest(req);
86
+ return validateSession(sessionId);
87
+ }
88
+
89
+ // Public paths that don't require authentication
90
+ const PUBLIC_PATHS = ['/login', '/login.html', '/register', '/register.html', '/reset-password', '/reset-password.html', '/verify-email', '/verify-email.html', '/docs', '/docs.html', '/docs.md', '/css/styles.css', '/manifest.json', '/sw.js', '/icon-192.png', '/icon-512.png', '/favicon.svg', '/favicon.ico'];
91
+
92
+ function requireAuth(req, res) {
93
+ const url = req.url.split('?')[0];
94
+
95
+ // Allow public paths
96
+ if (PUBLIC_PATHS.includes(url)) {
97
+ return false; // Don't require auth
98
+ }
99
+
100
+ // Check if authenticated
101
+ if (isAuthenticated(req)) {
102
+ return false; // Don't require auth (already authenticated)
103
+ }
104
+
105
+ // Redirect to login for HTML requests, return 401 for API requests
106
+ if (req.headers.accept && req.headers.accept.includes('text/html')) {
107
+ res.writeHead(302, { 'Location': '/login' });
108
+ res.end();
109
+ } else {
110
+ res.writeHead(401, { 'Content-Type': 'application/json' });
111
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
112
+ }
113
+
114
+ return true; // Auth required but not authenticated
115
+ }
116
+
117
+ module.exports = {
118
+ createSession,
119
+ validateSession,
120
+ getSession,
121
+ destroySession,
122
+ validateCredentials,
123
+ getSessionFromRequest,
124
+ isAuthenticated,
125
+ requireAuth,
126
+ hashPassword
127
+ };
package/lib/config.js ADDED
@@ -0,0 +1,53 @@
1
+ const path = require('path');
2
+ const crypto = require('crypto');
3
+
4
+ module.exports = {
5
+ PORT: process.env.PORT || 3847,
6
+ LOG_FILE: '/var/log/auto-ticket.log',
7
+ AGENT_LOGS_DIR: path.join(__dirname, '..', 'agent-logs'),
8
+ HISTORY_FILE: path.join(__dirname, '..', 'agent-history.json'),
9
+ MAX_HISTORY: 100,
10
+ CACHE_TTL: 60000, // 1 minute
11
+
12
+ // Redis configuration (dedicated instance)
13
+ REDIS_HOST: process.env.REDIS_HOST || 'localhost',
14
+ REDIS_PORT: parseInt(process.env.REDIS_PORT || '6379'),
15
+ REDIS_DB: parseInt(process.env.REDIS_DB || '0'),
16
+
17
+ // PostgreSQL configuration
18
+ DATABASE_URL: process.env.DATABASE_URL || 'postgresql://agentdev:agentdev_secure_password_change_me@localhost:6432/agentdev',
19
+
20
+ // JWT configuration for agent tokens
21
+ JWT_SECRET: process.env.JWT_SECRET || crypto.randomBytes(32).toString('hex'),
22
+ JWT_EXPIRES_IN: '90d', // Agent tokens valid for 90 days
23
+
24
+ // Encryption key for OAuth secrets
25
+ ENCRYPTION_KEY: process.env.ENCRYPTION_SECRET_KEY || crypto.randomBytes(32).toString('hex'),
26
+
27
+ // GitHub project configuration
28
+ GITHUB_ORG: 'data-tamer',
29
+ PROJECT_NUMBER: 1,
30
+ PROJECT_ID: 'PVT_kwDOCJIWbs4AnuSZ',
31
+ STATUS_FIELD_ID: 'PVTSSF_lADOCJIWbs4AnuSZzgfaWGs',
32
+ STATUS_OPTIONS: {
33
+ TODO: 'f75ad846',
34
+ IN_PROGRESS: '47fc9ee4',
35
+ TEST: 'c48bc058',
36
+ DONE: '98236657'
37
+ },
38
+
39
+ // Authentication configuration
40
+ AUTH: {
41
+ USERNAME: 'riccardo.ravaro@datatamer.ai',
42
+ PASSWORD: 'Nettuno1999',
43
+ SESSION_SECRET: crypto.randomBytes(32).toString('hex'),
44
+ SESSION_TTL: 24 * 60 * 60 * 1000 // 24 hours
45
+ },
46
+
47
+ // Device flow configuration
48
+ DEVICE_CODE_TTL: 600, // 10 minutes
49
+ DEVICE_CODE_POLL_INTERVAL: 5, // 5 seconds
50
+
51
+ // Base URL for device authorization
52
+ BASE_URL: process.env.BASE_URL || 'http://localhost:3847'
53
+ };