agentopia 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 (140) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/dist/app.d.ts +10 -0
  3. package/dist/app.d.ts.map +1 -0
  4. package/dist/app.js +121 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/config.d.ts +9 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +19 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/db/database.d.ts +5 -0
  11. package/dist/db/database.d.ts.map +1 -0
  12. package/dist/db/database.js +39 -0
  13. package/dist/db/database.js.map +1 -0
  14. package/dist/db/schema.d.ts +3 -0
  15. package/dist/db/schema.d.ts.map +1 -0
  16. package/dist/db/schema.js +621 -0
  17. package/dist/db/schema.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +49 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/logger.d.ts +4 -0
  23. package/dist/logger.d.ts.map +1 -0
  24. package/dist/logger.js +9 -0
  25. package/dist/logger.js.map +1 -0
  26. package/dist/middleware/auth.d.ts +13 -0
  27. package/dist/middleware/auth.d.ts.map +1 -0
  28. package/dist/middleware/auth.js +733 -0
  29. package/dist/middleware/auth.js.map +1 -0
  30. package/dist/routes/agents.d.ts +3 -0
  31. package/dist/routes/agents.d.ts.map +1 -0
  32. package/dist/routes/agents.js +1058 -0
  33. package/dist/routes/agents.js.map +1 -0
  34. package/dist/routes/issues.d.ts +4 -0
  35. package/dist/routes/issues.d.ts.map +1 -0
  36. package/dist/routes/issues.js +946 -0
  37. package/dist/routes/issues.js.map +1 -0
  38. package/dist/routes/knowledge.d.ts +3 -0
  39. package/dist/routes/knowledge.d.ts.map +1 -0
  40. package/dist/routes/knowledge.js +117 -0
  41. package/dist/routes/knowledge.js.map +1 -0
  42. package/dist/routes/memories.d.ts +3 -0
  43. package/dist/routes/memories.d.ts.map +1 -0
  44. package/dist/routes/memories.js +115 -0
  45. package/dist/routes/memories.js.map +1 -0
  46. package/dist/routes/messages.d.ts +3 -0
  47. package/dist/routes/messages.d.ts.map +1 -0
  48. package/dist/routes/messages.js +130 -0
  49. package/dist/routes/messages.js.map +1 -0
  50. package/dist/routes/projects.d.ts +3 -0
  51. package/dist/routes/projects.d.ts.map +1 -0
  52. package/dist/routes/projects.js +754 -0
  53. package/dist/routes/projects.js.map +1 -0
  54. package/dist/routes/templates.d.ts +3 -0
  55. package/dist/routes/templates.d.ts.map +1 -0
  56. package/dist/routes/templates.js +117 -0
  57. package/dist/routes/templates.js.map +1 -0
  58. package/dist/routes/ui.d.ts +3 -0
  59. package/dist/routes/ui.d.ts.map +1 -0
  60. package/dist/routes/ui.js +38 -0
  61. package/dist/routes/ui.js.map +1 -0
  62. package/dist/services/agent-hierarchy.d.ts +14 -0
  63. package/dist/services/agent-hierarchy.d.ts.map +1 -0
  64. package/dist/services/agent-hierarchy.js +58 -0
  65. package/dist/services/agent-hierarchy.js.map +1 -0
  66. package/dist/services/agent-issue-batch.d.ts +17 -0
  67. package/dist/services/agent-issue-batch.d.ts.map +1 -0
  68. package/dist/services/agent-issue-batch.js +57 -0
  69. package/dist/services/agent-issue-batch.js.map +1 -0
  70. package/dist/services/controller.d.ts +4 -0
  71. package/dist/services/controller.d.ts.map +1 -0
  72. package/dist/services/controller.js +237 -0
  73. package/dist/services/controller.js.map +1 -0
  74. package/dist/services/langgraph-runner.d.ts +33 -0
  75. package/dist/services/langgraph-runner.d.ts.map +1 -0
  76. package/dist/services/langgraph-runner.js +478 -0
  77. package/dist/services/langgraph-runner.js.map +1 -0
  78. package/dist/services/orchestrator.d.ts +9 -0
  79. package/dist/services/orchestrator.d.ts.map +1 -0
  80. package/dist/services/orchestrator.js +116 -0
  81. package/dist/services/orchestrator.js.map +1 -0
  82. package/dist/services/pre-controller.d.ts +7 -0
  83. package/dist/services/pre-controller.d.ts.map +1 -0
  84. package/dist/services/pre-controller.js +101 -0
  85. package/dist/services/pre-controller.js.map +1 -0
  86. package/dist/services/process-manager.d.ts +67 -0
  87. package/dist/services/process-manager.d.ts.map +1 -0
  88. package/dist/services/process-manager.js +938 -0
  89. package/dist/services/process-manager.js.map +1 -0
  90. package/dist/services/project-permissions.d.ts +84 -0
  91. package/dist/services/project-permissions.d.ts.map +1 -0
  92. package/dist/services/project-permissions.js +129 -0
  93. package/dist/services/project-permissions.js.map +1 -0
  94. package/dist/services/scheduler.d.ts +6 -0
  95. package/dist/services/scheduler.d.ts.map +1 -0
  96. package/dist/services/scheduler.js +300 -0
  97. package/dist/services/scheduler.js.map +1 -0
  98. package/dist/services/system-prompt.d.ts +3 -0
  99. package/dist/services/system-prompt.d.ts.map +1 -0
  100. package/dist/services/system-prompt.js +285 -0
  101. package/dist/services/system-prompt.js.map +1 -0
  102. package/dist/services/terminal.d.ts +18 -0
  103. package/dist/services/terminal.d.ts.map +1 -0
  104. package/dist/services/terminal.js +222 -0
  105. package/dist/services/terminal.js.map +1 -0
  106. package/dist/services/websocket.d.ts +15 -0
  107. package/dist/services/websocket.d.ts.map +1 -0
  108. package/dist/services/websocket.js +204 -0
  109. package/dist/services/websocket.js.map +1 -0
  110. package/dist/types.d.ts +108 -0
  111. package/dist/types.d.ts.map +1 -0
  112. package/dist/types.js +3 -0
  113. package/dist/types.js.map +1 -0
  114. package/env.ini +18 -0
  115. package/package.json +38 -0
  116. package/project_id +0 -0
  117. package/public/admin-users.html +188 -0
  118. package/public/agent.html +199 -0
  119. package/public/css/issues.css +275 -0
  120. package/public/css/style.css +1299 -0
  121. package/public/index.html +166 -0
  122. package/public/issue.html +76 -0
  123. package/public/js/agent.js +19 -0
  124. package/public/js/common.js +735 -0
  125. package/public/js/dashboard.js +772 -0
  126. package/public/js/files-panel.js +703 -0
  127. package/public/js/interactive-terminal.js +201 -0
  128. package/public/js/issue-renderer.js +559 -0
  129. package/public/js/issue.js +57 -0
  130. package/public/js/project.js +2425 -0
  131. package/public/js/terminal.js +564 -0
  132. package/public/project.html +430 -0
  133. package/public/terminal.html +67 -0
  134. package/public/vendor/marked.js +74 -0
  135. package/public/vendor/xterm-addon-fit.js +2 -0
  136. package/public/vendor/xterm.css +209 -0
  137. package/public/vendor/xterm.js +2 -0
  138. package/send_message_and_update_issue.js +65 -0
  139. package/tsconfig.json +19 -0
  140. package/update_round2_and_create_round3.js +284 -0
@@ -0,0 +1,733 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isLocalhostSafeRoute = isLocalhostSafeRoute;
7
+ exports.isLocalhostRequest = isLocalhostRequest;
8
+ exports.isLocalhostBypassRequest = isLocalhostBypassRequest;
9
+ exports.isLegacyAuthUser = isLegacyAuthUser;
10
+ exports.getRequestUser = getRequestUser;
11
+ exports.setupAuth = setupAuth;
12
+ const node_crypto_1 = require("node:crypto");
13
+ const uuid_1 = require("uuid");
14
+ const fs_1 = __importDefault(require("fs"));
15
+ const path_1 = __importDefault(require("path"));
16
+ const os_1 = __importDefault(require("os"));
17
+ const logger_1 = __importDefault(require("../logger"));
18
+ const COOKIE_NAME = 'argus-auth';
19
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.argus');
20
+ const CONFIG_PATH = path_1.default.join(CONFIG_DIR, 'config.json');
21
+ // --- Password hashing with scrypt ---
22
+ function hashPassword(pwd, salt) {
23
+ const s = salt || (0, node_crypto_1.randomBytes)(16).toString('hex');
24
+ const derived = (0, node_crypto_1.scryptSync)(pwd, s, 64).toString('hex');
25
+ return { hash: derived, salt: s };
26
+ }
27
+ function verifyPassword(pwd, storedHash, salt) {
28
+ const { hash } = hashPassword(pwd, salt);
29
+ const a = Buffer.from(hash, 'hex');
30
+ const b = Buffer.from(storedHash, 'hex');
31
+ if (a.length !== b.length)
32
+ return false;
33
+ return (0, node_crypto_1.timingSafeEqual)(a, b);
34
+ }
35
+ // Backward compat: detect old SHA-256 hashes (64 hex chars, no salt)
36
+ function isLegacySha256(config) {
37
+ return !!config.passwordHash && !config.passwordSalt;
38
+ }
39
+ function legacySha256(pwd) {
40
+ return (0, node_crypto_1.createHash)('sha256').update(pwd).digest('hex');
41
+ }
42
+ function parseCookies(header) {
43
+ const cookies = {};
44
+ if (!header)
45
+ return cookies;
46
+ for (const part of header.split(';')) {
47
+ const idx = part.indexOf('=');
48
+ if (idx === -1)
49
+ continue;
50
+ cookies[part.slice(0, idx).trim()] = part.slice(idx + 1).trim();
51
+ }
52
+ return cookies;
53
+ }
54
+ function loadAuthConfig() {
55
+ try {
56
+ const { getDatabase } = require('../db/database');
57
+ const db = getDatabase();
58
+ const row = db.prepare("SELECT value FROM settings WHERE key = 'auth'").get();
59
+ if (row)
60
+ return JSON.parse(row.value);
61
+ }
62
+ catch (e) {
63
+ logger_1.default.error(e, 'Failed to load auth config from database');
64
+ }
65
+ // Fallback: try legacy file config and migrate
66
+ try {
67
+ if (fs_1.default.existsSync(CONFIG_PATH)) {
68
+ const config = JSON.parse(fs_1.default.readFileSync(CONFIG_PATH, 'utf-8'));
69
+ if (config.passwordHash) {
70
+ logger_1.default.info('Migrating auth config from file to database');
71
+ saveAuthConfig(config);
72
+ return config;
73
+ }
74
+ }
75
+ }
76
+ catch { }
77
+ return {};
78
+ }
79
+ function saveAuthConfig(config) {
80
+ try {
81
+ const { getDatabase } = require('../db/database');
82
+ const db = getDatabase();
83
+ db.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('auth', ?)").run(JSON.stringify(config));
84
+ }
85
+ catch (e) {
86
+ logger_1.default.error(e, 'Failed to save auth config to database');
87
+ }
88
+ }
89
+ // --- Localhost bypass: only safe (agent-usable) routes ---
90
+ const LOCALHOST_SAFE_PREFIXES = [
91
+ '/api/projects', // project CRUD and sub-resources (issues, agents, etc.)
92
+ '/api/issues/', // issue CRUD + comments
93
+ '/api/agents/', // agent status/logs (GET), start/stop (POST)
94
+ '/api/comments/', // comment editing
95
+ '/api/milestones', // milestone CRUD
96
+ '/api/notifications',
97
+ '/api/reactions/',
98
+ '/api/inbox',
99
+ '/api/knowledge/',
100
+ '/api/my-issues',
101
+ ];
102
+ // Admin-only operations that localhost should NOT bypass
103
+ const LOCALHOST_BLOCKED_PATTERNS = [
104
+ { method: 'POST', prefix: '/api/auth/' },
105
+ { method: 'GET', prefix: '/api/auth/' },
106
+ ];
107
+ function isLocalhostSafeRoute(method, url) {
108
+ for (const pattern of LOCALHOST_BLOCKED_PATTERNS) {
109
+ if (method === pattern.method && url.startsWith(pattern.prefix)) {
110
+ return false;
111
+ }
112
+ }
113
+ for (const prefix of LOCALHOST_SAFE_PREFIXES) {
114
+ if (url.startsWith(prefix))
115
+ return true;
116
+ }
117
+ if (url.startsWith('/ws/'))
118
+ return true;
119
+ return false;
120
+ }
121
+ function isLocalhostRequest(request) {
122
+ const remoteIp = request.ip;
123
+ return remoteIp === '127.0.0.1' || remoteIp === '::1' || remoteIp === '::ffff:127.0.0.1';
124
+ }
125
+ function isLocalhostBypassRequest(request) {
126
+ return isLocalhostRequest(request) && isLocalhostSafeRoute(request.method, request.url);
127
+ }
128
+ function isLegacyAuthUser(user) {
129
+ return !!user && user.id === 'legacy';
130
+ }
131
+ function getRequestToken(request) {
132
+ const cookies = parseCookies(request.headers.cookie);
133
+ let token = cookies[COOKIE_NAME];
134
+ if (!token) {
135
+ const authHeader = request.headers.authorization;
136
+ if (authHeader?.startsWith('Bearer ')) {
137
+ token = authHeader.slice(7);
138
+ }
139
+ }
140
+ return token || null;
141
+ }
142
+ function getRequestUser(request) {
143
+ try {
144
+ const token = getRequestToken(request);
145
+ if (!token)
146
+ return null;
147
+ const { getDatabase } = require('../db/database');
148
+ const db = getDatabase();
149
+ const session = db.prepare('SELECT user_id FROM sessions WHERE token = ? AND expires_at > ?').get(token, Date.now());
150
+ if (session?.user_id) {
151
+ return db.prepare('SELECT * FROM users WHERE id = ?').get(session.user_id) || null;
152
+ }
153
+ const currentAuthConfig = loadAuthConfig();
154
+ if (currentAuthConfig.passwordHash && token === currentAuthConfig.passwordHash) {
155
+ return {
156
+ id: 'legacy',
157
+ username: 'admin',
158
+ email: '',
159
+ password_hash: '',
160
+ password_salt: '',
161
+ display_name: 'Admin',
162
+ role: 'admin',
163
+ created_at: '',
164
+ last_login_at: null,
165
+ };
166
+ }
167
+ return null;
168
+ }
169
+ catch {
170
+ return null;
171
+ }
172
+ }
173
+ // --- HTML pages ---
174
+ const THEME_SCRIPT = `<script>
175
+ (function() {
176
+ var themes = {
177
+ 'github-dark': { bg:'#0d1117', fg:'#e6edf3', headerBg:'#161b22', border:'#30363d', textSecondary:'#8b949e', accent:'#58a6ff', error:'#f85149' },
178
+ 'dracula': { bg:'#282a36', fg:'#f8f8f2', headerBg:'#21222c', border:'#44475a', textSecondary:'#6272a4', accent:'#8be9fd', error:'#ff5555' },
179
+ 'nord': { bg:'#2e3440', fg:'#d8dee9', headerBg:'#3b4252', border:'#4c566a', textSecondary:'#81a1c1', accent:'#88c0d0', error:'#bf616a' },
180
+ 'monokai': { bg:'#272822', fg:'#f8f8f2', headerBg:'#1e1f1c', border:'#3e3d32', textSecondary:'#75715e', accent:'#66d9ef', error:'#f92672' },
181
+ 'solarized-dark': { bg:'#002b36', fg:'#839496', headerBg:'#073642', border:'#586e75', textSecondary:'#657b83', accent:'#268bd2', error:'#dc322f' },
182
+ 'solarized-light': { bg:'#fdf6e3', fg:'#073642', headerBg:'#eee8d5', border:'#c9bba3', textSecondary:'#586e75', accent:'#268bd2', error:'#dc322f' }
183
+ };
184
+ var name = null;
185
+ try { name = localStorage.getItem('argus-theme'); } catch(e) {}
186
+ var t = themes[name] || themes['solarized-light'];
187
+ var r = document.documentElement;
188
+ r.style.setProperty('--bg', t.bg);
189
+ r.style.setProperty('--fg', t.fg);
190
+ r.style.setProperty('--header-bg', t.headerBg);
191
+ r.style.setProperty('--border', t.border);
192
+ r.style.setProperty('--text-secondary', t.textSecondary);
193
+ r.style.setProperty('--accent', t.accent);
194
+ r.style.setProperty('--error', t.error);
195
+ })();
196
+ </script>`;
197
+ const PAGE_STYLE = `
198
+ * { margin: 0; padding: 0; box-sizing: border-box; }
199
+ body { font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Fira Code', Menlo, monospace; background: var(--bg, #0d1117); color: var(--fg, #e6edf3); display: flex; align-items: center; justify-content: center; min-height: 100vh; }
200
+ .card { background: var(--header-bg, #161b22); border: 1px solid var(--border, #30363d); border-radius: 12px; padding: 2rem; width: 100%; max-width: 360px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); }
201
+ h1 { font-size: 1.5rem; margin-bottom: 0.5rem; text-align: center; }
202
+ h1 span { color: var(--accent, #58a6ff); }
203
+ .subtitle { font-size: 0.875rem; color: var(--text-secondary, #8b949e); text-align: center; margin-bottom: 1.5rem; }
204
+ label { display: block; margin-bottom: 0.5rem; font-size: 0.875rem; color: var(--text-secondary, #8b949e); }
205
+ input[type="password"] { width: 100%; padding: 0.75rem; border: 1px solid var(--border, #30363d); border-radius: 8px; background: var(--bg, #0d1117); color: var(--fg, #e6edf3); font-size: 1rem; outline: none; margin-bottom: 0.75rem; font-family: inherit; }
206
+ input[type="password"]:focus { border-color: var(--accent, #58a6ff); }
207
+ button { width: 100%; padding: 0.75rem; margin-top: 0.5rem; border: none; border-radius: 8px; background: #238636; color: #fff; font-size: 1rem; cursor: pointer; font-weight: 600; font-family: inherit; }
208
+ button:hover { background: #2ea043; }
209
+ .error { color: var(--error, #f85149); font-size: 0.875rem; margin-top: 0.75rem; text-align: center; display: none; }
210
+ .success { color: var(--accent, #58a6ff); font-size: 0.875rem; margin-top: 0.75rem; text-align: center; display: none; }
211
+ `;
212
+ const SETUP_HTML = `<!DOCTYPE html>
213
+ <html lang="en">
214
+ <head>
215
+ <meta charset="UTF-8">
216
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
217
+ <title>Argus — Setup</title>
218
+ <style>${PAGE_STYLE}</style>
219
+ ${THEME_SCRIPT}
220
+ </head>
221
+ <body>
222
+ <div class="card">
223
+ <h1><span>Argus</span></h1>
224
+ <p class="subtitle">Set a password to protect your platform</p>
225
+ <form id="form">
226
+ <label for="password">Password</label>
227
+ <input type="password" id="password" name="password" placeholder="Enter password (min 4 chars)" autofocus required>
228
+ <label for="confirm">Confirm password</label>
229
+ <input type="password" id="confirm" name="confirm" placeholder="Confirm password" required>
230
+ <button type="submit">Set Password</button>
231
+ <div class="error" id="error"></div>
232
+ </form>
233
+ </div>
234
+ <script>
235
+ document.getElementById('form').addEventListener('submit', async (e) => {
236
+ e.preventDefault();
237
+ const errEl = document.getElementById('error');
238
+ const password = document.getElementById('password').value;
239
+ const confirm = document.getElementById('confirm').value;
240
+ if (password.length < 4) { errEl.textContent = 'Password must be at least 4 characters'; errEl.style.display = 'block'; return; }
241
+ if (password !== confirm) { errEl.textContent = 'Passwords do not match'; errEl.style.display = 'block'; return; }
242
+ const res = await fetch('/api/auth/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) });
243
+ if (res.ok) { window.location.href = '/'; } else { const data = await res.json(); errEl.textContent = data.error || 'Setup failed'; errEl.style.display = 'block'; }
244
+ });
245
+ </script>
246
+ </body>
247
+ </html>`;
248
+ const LOGIN_HTML = `<!DOCTYPE html>
249
+ <html lang="en">
250
+ <head>
251
+ <meta charset="UTF-8">
252
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
253
+ <title>Argus — Login</title>
254
+ <style>${PAGE_STYLE}
255
+ input[type="text"] { width: 100%; padding: 0.75rem; border: 1px solid var(--border, #30363d); border-radius: 8px; background: var(--bg, #0d1117); color: var(--fg, #e6edf3); font-size: 1rem; outline: none; margin-bottom: 0.75rem; font-family: inherit; }
256
+ input[type="text"]:focus { border-color: var(--accent, #58a6ff); }
257
+ </style>
258
+ ${THEME_SCRIPT}
259
+ </head>
260
+ <body>
261
+ <div class="card">
262
+ <h1><span>Argus</span></h1>
263
+ <form id="form">
264
+ <div id="username-field">
265
+ <label for="username">Username</label>
266
+ <input type="text" id="username" name="username" autofocus required>
267
+ </div>
268
+ <label for="password">Password</label>
269
+ <input type="password" id="password" name="password" required>
270
+ <button type="submit">Login</button>
271
+ <div class="error" id="error"></div>
272
+ <p style="text-align:center;margin-top:1rem;font-size:0.875rem;color:var(--text-secondary,#8b949e)">Don't have an account? <a href="/register" style="color:var(--accent,#58a6ff)">Register</a></p>
273
+ </form>
274
+ </div>
275
+ <script>
276
+ document.getElementById('form').addEventListener('submit', async (e) => {
277
+ e.preventDefault();
278
+ const errEl = document.getElementById('error');
279
+ errEl.style.display = 'none';
280
+ const username = document.getElementById('username')?.value;
281
+ const password = document.getElementById('password').value;
282
+ // Try multi-user login first
283
+ if (username) {
284
+ const res = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) });
285
+ if (res.ok) { window.location.href = '/'; return; }
286
+ }
287
+ // Fallback: legacy single-password login
288
+ const res2 = await fetch('/api/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) });
289
+ if (res2.ok) { window.location.href = '/'; return; }
290
+ errEl.textContent = 'Invalid username or password';
291
+ errEl.style.display = 'block';
292
+ });
293
+ </script>
294
+ </body>
295
+ </html>`;
296
+ const REGISTER_HTML = `<!DOCTYPE html>
297
+ <html lang="en">
298
+ <head>
299
+ <meta charset="UTF-8">
300
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
301
+ <title>Argus — Register</title>
302
+ <style>${PAGE_STYLE}</style>
303
+ ${THEME_SCRIPT}
304
+ </head>
305
+ <body>
306
+ <div class="card">
307
+ <h1><span>Argus</span></h1>
308
+ <p class="subtitle">Create your account</p>
309
+ <form id="form">
310
+ <label for="username">Username</label>
311
+ <input type="text" id="username" name="username" placeholder="2-32 characters" autofocus required style="width:100%;padding:0.75rem;border:1px solid var(--border,#30363d);border-radius:8px;background:var(--bg,#0d1117);color:var(--fg,#e6edf3);font-size:1rem;outline:none;margin-bottom:0.75rem;font-family:inherit;">
312
+ <label for="display_name">Display Name (optional)</label>
313
+ <input type="text" id="display_name" name="display_name" style="width:100%;padding:0.75rem;border:1px solid var(--border,#30363d);border-radius:8px;background:var(--bg,#0d1117);color:var(--fg,#e6edf3);font-size:1rem;outline:none;margin-bottom:0.75rem;font-family:inherit;">
314
+ <label for="password">Password</label>
315
+ <input type="password" id="password" name="password" placeholder="Min 4 characters" required>
316
+ <label for="confirm">Confirm Password</label>
317
+ <input type="password" id="confirm" name="confirm" required>
318
+ <button type="submit">Register</button>
319
+ <div class="error" id="error"></div>
320
+ <p style="text-align:center;margin-top:1rem;font-size:0.875rem;color:var(--text-secondary)">Already have an account? <a href="/login" style="color:var(--accent)">Login</a></p>
321
+ </form>
322
+ </div>
323
+ <script>
324
+ document.getElementById('form').addEventListener('submit', async (e) => {
325
+ e.preventDefault();
326
+ const errEl = document.getElementById('error');
327
+ const username = document.getElementById('username').value;
328
+ const display_name = document.getElementById('display_name').value;
329
+ const password = document.getElementById('password').value;
330
+ const confirm = document.getElementById('confirm').value;
331
+ if (password.length < 4) { errEl.textContent = 'Password must be at least 4 characters'; errEl.style.display = 'block'; return; }
332
+ if (password !== confirm) { errEl.textContent = 'Passwords do not match'; errEl.style.display = 'block'; return; }
333
+ const res = await fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password, display_name: display_name || undefined }) });
334
+ if (res.ok) { window.location.href = '/'; } else { const data = await res.json(); errEl.textContent = data.error || 'Registration failed'; errEl.style.display = 'block'; }
335
+ });
336
+ </script>
337
+ </body>
338
+ </html>`;
339
+ const CHANGE_PASSWORD_HTML = `<!DOCTYPE html>
340
+ <html lang="en">
341
+ <head>
342
+ <meta charset="UTF-8">
343
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
344
+ <title>Argus — Change Password</title>
345
+ <style>${PAGE_STYLE}</style>
346
+ ${THEME_SCRIPT}
347
+ </head>
348
+ <body>
349
+ <div class="card">
350
+ <h1><span>Argus</span></h1>
351
+ <p class="subtitle">Change your password</p>
352
+ <form id="form">
353
+ <label for="current">Current password</label>
354
+ <input type="password" id="current" name="current" autofocus required>
355
+ <label for="password">New password</label>
356
+ <input type="password" id="password" name="password" placeholder="Min 4 characters" required>
357
+ <label for="confirm">Confirm new password</label>
358
+ <input type="password" id="confirm" name="confirm" required>
359
+ <button type="submit">Change Password</button>
360
+ <div class="error" id="error"></div>
361
+ <div class="success" id="success">Password changed successfully</div>
362
+ </form>
363
+ </div>
364
+ <script>
365
+ document.getElementById('form').addEventListener('submit', async (e) => {
366
+ e.preventDefault();
367
+ const errEl = document.getElementById('error');
368
+ const successEl = document.getElementById('success');
369
+ errEl.style.display = 'none'; successEl.style.display = 'none';
370
+ const current = document.getElementById('current').value;
371
+ const password = document.getElementById('password').value;
372
+ const confirm = document.getElementById('confirm').value;
373
+ if (password.length < 4) { errEl.textContent = 'New password must be at least 4 characters'; errEl.style.display = 'block'; return; }
374
+ if (password !== confirm) { errEl.textContent = 'Passwords do not match'; errEl.style.display = 'block'; return; }
375
+ const res = await fetch('/api/auth/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current, password }) });
376
+ if (res.ok) { successEl.style.display = 'block'; document.getElementById('form').reset(); } else { const data = await res.json(); errEl.textContent = data.error || 'Failed'; errEl.style.display = 'block'; }
377
+ });
378
+ </script>
379
+ </body>
380
+ </html>`;
381
+ /**
382
+ * Simplified auth: cookie stores passwordHash directly, no server-side sessions.
383
+ * Follows the same pattern as swarmie for maximum reliability.
384
+ */
385
+ function setupAuth(app) {
386
+ let authConfig = loadAuthConfig();
387
+ // Ensure sessions table has user_id column (migration may not have been applied yet)
388
+ try {
389
+ const { getDatabase } = require('../db/database');
390
+ const db = getDatabase();
391
+ const sessionCols = db.prepare("PRAGMA table_info(sessions)").all();
392
+ if (!sessionCols.find((c) => c.name === 'user_id')) {
393
+ db.exec("ALTER TABLE sessions ADD COLUMN user_id TEXT REFERENCES users(id) ON DELETE CASCADE");
394
+ db.exec("CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)");
395
+ logger_1.default.info('Auth: applied sessions.user_id migration');
396
+ }
397
+ // Backfill sessions with NULL user_id: if only one user exists, assign all orphan sessions to them
398
+ const nullCount = db.prepare("SELECT COUNT(*) as c FROM sessions WHERE user_id IS NULL").get().c;
399
+ if (nullCount > 0) {
400
+ const users = db.prepare("SELECT id FROM users").all();
401
+ if (users.length === 1) {
402
+ db.prepare("UPDATE sessions SET user_id = ? WHERE user_id IS NULL").run(users[0].id);
403
+ logger_1.default.info(`Auth: backfilled ${nullCount} sessions with user_id=${users[0].id}`);
404
+ }
405
+ }
406
+ }
407
+ catch (e) {
408
+ logger_1.default.warn(e, 'Failed to check/apply sessions migration');
409
+ }
410
+ function checkPassword(pwd) {
411
+ if (!authConfig.passwordHash)
412
+ return false;
413
+ if (isLegacySha256(authConfig)) {
414
+ return legacySha256(pwd) === authConfig.passwordHash;
415
+ }
416
+ return verifyPassword(pwd, authConfig.passwordHash, authConfig.passwordSalt);
417
+ }
418
+ function setPassword(pwd) {
419
+ const { hash, salt } = hashPassword(pwd);
420
+ authConfig = { passwordHash: hash, passwordSalt: salt };
421
+ saveAuthConfig(authConfig);
422
+ }
423
+ function setAuthCookie(reply) {
424
+ reply.header('Set-Cookie', `${COOKIE_NAME}=${authConfig.passwordHash}; HttpOnly; Path=/; SameSite=Lax`);
425
+ }
426
+ function isValidToken(token) {
427
+ return !!authConfig.passwordHash && token === authConfig.passwordHash;
428
+ }
429
+ // Setup page
430
+ app.get('/setup', async (_req, reply) => {
431
+ if (authConfig.passwordHash)
432
+ return reply.redirect('/login');
433
+ reply.type('text/html').send(SETUP_HTML);
434
+ });
435
+ // Setup endpoint
436
+ app.post('/api/auth/setup', async (request, reply) => {
437
+ if (authConfig.passwordHash)
438
+ return reply.status(403).send({ error: 'Password already set' });
439
+ const body = request.body;
440
+ if (!body?.password || body.password.length < 4) {
441
+ return reply.status(400).send({ error: 'Password must be at least 4 characters' });
442
+ }
443
+ setPassword(body.password);
444
+ logger_1.default.info('Password has been set');
445
+ setAuthCookie(reply);
446
+ reply.send({ ok: true });
447
+ });
448
+ // Register page
449
+ app.get('/register', async (_req, reply) => {
450
+ reply.type('text/html').send(REGISTER_HTML);
451
+ });
452
+ // Login page
453
+ app.get('/login', async (_req, reply) => {
454
+ if (!authConfig.passwordHash) {
455
+ // Check if multi-user mode has users
456
+ let hasUsers = false;
457
+ try {
458
+ const { getDatabase } = require('../db/database');
459
+ const db = getDatabase();
460
+ hasUsers = db.prepare('SELECT COUNT(*) as c FROM users').get().c > 0;
461
+ }
462
+ catch { }
463
+ if (!hasUsers)
464
+ return reply.redirect('/register');
465
+ }
466
+ reply.type('text/html').send(LOGIN_HTML);
467
+ });
468
+ // Login endpoint
469
+ app.post('/api/auth', async (request, reply) => {
470
+ if (!authConfig.passwordHash)
471
+ return reply.status(400).send({ error: 'No password configured' });
472
+ const body = request.body;
473
+ if (body?.password && checkPassword(body.password)) {
474
+ // Auto-migrate legacy SHA-256 to scrypt on successful login
475
+ if (isLegacySha256(authConfig)) {
476
+ setPassword(body.password);
477
+ logger_1.default.info('Migrated password hash from SHA-256 to scrypt');
478
+ }
479
+ setAuthCookie(reply);
480
+ reply.send({ ok: true, token: authConfig.passwordHash });
481
+ }
482
+ else {
483
+ reply.status(401).send({ error: 'Invalid password' });
484
+ }
485
+ });
486
+ // Change password page
487
+ app.get('/change-password', async (_req, reply) => {
488
+ reply.type('text/html').send(CHANGE_PASSWORD_HTML);
489
+ });
490
+ // Change password endpoint
491
+ app.post('/api/auth/change-password', async (request, reply) => {
492
+ if (!authConfig.passwordHash)
493
+ return reply.status(400).send({ error: 'No password configured' });
494
+ const body = request.body;
495
+ if (!body?.current || !checkPassword(body.current)) {
496
+ return reply.status(401).send({ error: 'Current password is incorrect' });
497
+ }
498
+ if (!body.password || body.password.length < 4) {
499
+ return reply.status(400).send({ error: 'New password must be at least 4 characters' });
500
+ }
501
+ setPassword(body.password);
502
+ logger_1.default.info('Password has been changed');
503
+ setAuthCookie(reply);
504
+ reply.send({ ok: true });
505
+ });
506
+ // Logout
507
+ app.post('/api/auth/logout', async (request, reply) => {
508
+ // Clean up session token from DB
509
+ const cookies = parseCookies(request.headers.cookie);
510
+ const token = cookies[COOKIE_NAME];
511
+ if (token) {
512
+ try {
513
+ const { getDatabase } = require('../db/database');
514
+ const db = getDatabase();
515
+ db.prepare('DELETE FROM sessions WHERE token = ?').run(token);
516
+ }
517
+ catch { }
518
+ }
519
+ reply.header('Set-Cookie', `${COOKIE_NAME}=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0`).send({ ok: true });
520
+ });
521
+ // --- Multi-user API ---
522
+ // Register user (first user becomes admin)
523
+ app.post('/api/auth/register', async (request, reply) => {
524
+ const body = request.body;
525
+ if (!body?.username || !body?.password) {
526
+ return reply.status(400).send({ error: 'username and password are required' });
527
+ }
528
+ if (body.password.length < 4) {
529
+ return reply.status(400).send({ error: 'Password must be at least 4 characters' });
530
+ }
531
+ if (!/^[a-zA-Z0-9_-]{2,32}$/.test(body.username)) {
532
+ return reply.status(400).send({ error: 'Username must be 2-32 characters (letters, numbers, -, _)' });
533
+ }
534
+ const { getDatabase } = require('../db/database');
535
+ const db = getDatabase();
536
+ // Check if username exists
537
+ const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(body.username);
538
+ if (existing)
539
+ return reply.status(409).send({ error: 'Username already taken' });
540
+ // First user becomes admin
541
+ const userCount = db.prepare('SELECT COUNT(*) as c FROM users').get().c;
542
+ const role = userCount === 0 ? 'admin' : 'member';
543
+ const userId = (0, uuid_1.v4)();
544
+ const { hash, salt } = hashPassword(body.password);
545
+ db.prepare('INSERT INTO users (id, username, email, password_hash, password_salt, display_name, role) VALUES (?, ?, ?, ?, ?, ?, ?)').run(userId, body.username, body.email || '', hash, salt, body.display_name || body.username, role);
546
+ // Auto-login: create session
547
+ const sessionToken = (0, node_crypto_1.randomBytes)(32).toString('hex');
548
+ const now = Date.now();
549
+ const expiresAt = now + 30 * 24 * 60 * 60 * 1000; // 30 days
550
+ db.prepare('INSERT INTO sessions (token, user_id, csrf_token, created_at, expires_at) VALUES (?, ?, ?, ?, ?)')
551
+ .run(sessionToken, userId, (0, node_crypto_1.randomBytes)(16).toString('hex'), now, expiresAt);
552
+ reply.header('Set-Cookie', `${COOKIE_NAME}=${sessionToken}; HttpOnly; Path=/; SameSite=Lax`);
553
+ const user = db.prepare('SELECT id, username, email, display_name, role, created_at FROM users WHERE id = ?').get(userId);
554
+ return reply.status(201).send({ ok: true, user, token: sessionToken });
555
+ });
556
+ // Login with username + password (multi-user)
557
+ app.post('/api/auth/login', async (request, reply) => {
558
+ const body = request.body;
559
+ if (!body?.username || !body?.password) {
560
+ return reply.status(400).send({ error: 'username and password are required' });
561
+ }
562
+ const { getDatabase } = require('../db/database');
563
+ const db = getDatabase();
564
+ const user = db.prepare('SELECT * FROM users WHERE username = ?').get(body.username);
565
+ if (!user || !verifyPassword(body.password, user.password_hash, user.password_salt)) {
566
+ return reply.status(401).send({ error: 'Invalid username or password' });
567
+ }
568
+ // Update last_login_at
569
+ db.prepare("UPDATE users SET last_login_at = datetime('now') WHERE id = ?").run(user.id);
570
+ // Create session
571
+ const sessionToken = (0, node_crypto_1.randomBytes)(32).toString('hex');
572
+ const now = Date.now();
573
+ const expiresAt = now + 30 * 24 * 60 * 60 * 1000;
574
+ db.prepare('INSERT INTO sessions (token, user_id, csrf_token, created_at, expires_at) VALUES (?, ?, ?, ?, ?)')
575
+ .run(sessionToken, user.id, (0, node_crypto_1.randomBytes)(16).toString('hex'), now, expiresAt);
576
+ reply.header('Set-Cookie', `${COOKIE_NAME}=${sessionToken}; HttpOnly; Path=/; SameSite=Lax`);
577
+ return { ok: true, token: sessionToken, user: { id: user.id, username: user.username, email: user.email, display_name: user.display_name, role: user.role } };
578
+ });
579
+ // Get current user info
580
+ app.get('/api/auth/me', async (request, reply) => {
581
+ // Try multi-user session first
582
+ const user = getUserFromRequest(request);
583
+ if (user) {
584
+ return { id: user.id, username: user.username, email: user.email, display_name: user.display_name, role: user.role, created_at: user.created_at };
585
+ }
586
+ // Fallback: legacy single-password auth (cookie = passwordHash)
587
+ const cookies = parseCookies(request.headers.cookie);
588
+ const token = cookies[COOKIE_NAME];
589
+ if (token && isValidToken(token)) {
590
+ return { id: 'legacy', username: 'admin', display_name: 'Admin', role: 'admin' };
591
+ }
592
+ return reply.status(401).send({ error: 'Not authenticated' });
593
+ });
594
+ // List users (admin only)
595
+ app.get('/api/auth/users', async (request, reply) => {
596
+ const user = getUserFromRequest(request);
597
+ if (!user || user.role !== 'admin')
598
+ return reply.status(403).send({ error: 'Admin access required' });
599
+ const { getDatabase } = require('../db/database');
600
+ const db = getDatabase();
601
+ const users = db.prepare('SELECT id, username, email, display_name, role, created_at, last_login_at FROM users ORDER BY created_at').all();
602
+ return { users };
603
+ });
604
+ // Update user role (admin only)
605
+ app.put('/api/auth/users/:id', async (request, reply) => {
606
+ const user = getUserFromRequest(request);
607
+ if (!user || user.role !== 'admin')
608
+ return reply.status(403).send({ error: 'Admin access required' });
609
+ const { id } = request.params;
610
+ const { role } = request.body;
611
+ if (id === user.id)
612
+ return reply.status(400).send({ error: 'Cannot change your own role' });
613
+ if (role && !['admin', 'member'].includes(role))
614
+ return reply.status(400).send({ error: 'Invalid role' });
615
+ const { getDatabase } = require('../db/database');
616
+ const db = getDatabase();
617
+ const target = db.prepare('SELECT id FROM users WHERE id = ?').get(id);
618
+ if (!target)
619
+ return reply.status(404).send({ error: 'User not found' });
620
+ if (role)
621
+ db.prepare('UPDATE users SET role = ? WHERE id = ?').run(role, id);
622
+ const updated = db.prepare('SELECT id, username, email, display_name, role, created_at, last_login_at FROM users WHERE id = ?').get(id);
623
+ return { user: updated };
624
+ });
625
+ // Delete user (admin only)
626
+ app.delete('/api/auth/users/:id', async (request, reply) => {
627
+ const user = getUserFromRequest(request);
628
+ if (!user || user.role !== 'admin')
629
+ return reply.status(403).send({ error: 'Admin access required' });
630
+ const { id } = request.params;
631
+ if (id === user.id)
632
+ return reply.status(400).send({ error: 'Cannot delete yourself' });
633
+ const { getDatabase } = require('../db/database');
634
+ const db = getDatabase();
635
+ const target = db.prepare('SELECT id FROM users WHERE id = ?').get(id);
636
+ if (!target)
637
+ return reply.status(404).send({ error: 'User not found' });
638
+ db.prepare('DELETE FROM sessions WHERE user_id = ?').run(id);
639
+ db.prepare('DELETE FROM users WHERE id = ?').run(id);
640
+ return { ok: true };
641
+ });
642
+ // Helper: resolve user from request token
643
+ function getUserFromRequest(request) {
644
+ return getRequestUser(request);
645
+ }
646
+ // Check if a session token is valid (for multi-user mode)
647
+ function isValidSessionToken(token) {
648
+ try {
649
+ const { getDatabase } = require('../db/database');
650
+ const db = getDatabase();
651
+ const session = db.prepare('SELECT token FROM sessions WHERE token = ? AND expires_at > ?').get(token, Date.now());
652
+ return !!session;
653
+ }
654
+ catch {
655
+ return false;
656
+ }
657
+ }
658
+ // Auth hook
659
+ app.addHook('onRequest', async (request, reply) => {
660
+ const url = request.url;
661
+ // ARGUS_NO_AUTH=true: skip all authentication
662
+ if (process.env.ARGUS_NO_AUTH === 'true') {
663
+ return;
664
+ }
665
+ // Allow auth routes and favicon
666
+ if (request.method === 'OPTIONS' || url === '/login' || url === '/setup' || url === '/register' || url.startsWith('/api/auth') || url === '/favicon.ico') {
667
+ return;
668
+ }
669
+ // Localhost bypass: only for agent-safe routes
670
+ if (isLocalhostBypassRequest(request)) {
671
+ return;
672
+ }
673
+ // No password in memory -> reload from DB
674
+ if (!authConfig.passwordHash) {
675
+ authConfig = loadAuthConfig();
676
+ }
677
+ if (!authConfig.passwordHash) {
678
+ // Check if multi-user mode has users
679
+ let hasUsers = false;
680
+ try {
681
+ const { getDatabase } = require('../db/database');
682
+ const db = getDatabase();
683
+ const count = db.prepare('SELECT COUNT(*) as c FROM users').get().c;
684
+ hasUsers = count > 0;
685
+ }
686
+ catch { }
687
+ if (!hasUsers) {
688
+ if (url.startsWith('/api/') || url.startsWith('/ws')) {
689
+ reply.status(401).send({ error: 'No authentication configured. Visit /register to create the first account.' });
690
+ }
691
+ else {
692
+ reply.redirect('/register');
693
+ }
694
+ return;
695
+ }
696
+ }
697
+ // Check cookie token
698
+ const cookies = parseCookies(request.headers.cookie);
699
+ const token = cookies[COOKIE_NAME];
700
+ // Try legacy single-password token first
701
+ if (token && isValidToken(token)) {
702
+ return;
703
+ }
704
+ // Try multi-user session token
705
+ if (token && isValidSessionToken(token)) {
706
+ return;
707
+ }
708
+ // Check Authorization: Bearer <token>
709
+ const authHeader = request.headers.authorization;
710
+ if (authHeader?.startsWith('Bearer ')) {
711
+ const bearerToken = authHeader.slice(7);
712
+ if (isValidToken(bearerToken))
713
+ return;
714
+ if (isValidSessionToken(bearerToken))
715
+ return;
716
+ }
717
+ // Check query token (for WebSocket connections)
718
+ const queryToken = request.query?.token;
719
+ if (queryToken && (isValidToken(queryToken) || isValidSessionToken(queryToken)))
720
+ return;
721
+ // Allow static assets and UI page routes — only protect API/WS
722
+ if (url.startsWith('/public/') || url.startsWith('/css/') || url.startsWith('/js/') || url.startsWith('/vendor/')) {
723
+ return;
724
+ }
725
+ // Unauthenticated page routes → redirect to login
726
+ if (!url.startsWith('/api/') && !url.startsWith('/ws')) {
727
+ return reply.redirect('/login');
728
+ }
729
+ // Unauthenticated API/WS
730
+ reply.status(401).send({ error: 'Unauthorized' });
731
+ });
732
+ }
733
+ //# sourceMappingURL=auth.js.map