neoagent 3.0.1-beta.8 → 3.1.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/.env.example +2 -10
- package/README.md +12 -3
- package/docs/automation.md +37 -0
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +34 -8
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +10 -8
- package/docs/operations.md +1 -1
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +138 -0
- package/flutter_app/lib/main_app_shell.dart +38 -13
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1612 -214
- package/flutter_app/lib/main_controller.dart +398 -26
- package/flutter_app/lib/main_devices.dart +293 -207
- package/flutter_app/lib/main_models.dart +142 -0
- package/flutter_app/lib/main_navigation.dart +19 -1
- package/flutter_app/lib/main_operations.dart +288 -9
- package/flutter_app/lib/main_settings.dart +510 -300
- package/flutter_app/lib/main_shared.dart +2 -0
- package/flutter_app/lib/main_timeline.dart +1378 -0
- package/flutter_app/lib/src/backend_client.dart +149 -19
- package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
- package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
- package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
- package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
- package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
- package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
- package/flutter_app/lib/src/recording_payloads.dart +9 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
- package/flutter_app/windows/runner/flutter_window.cpp +75 -0
- package/landing/index.html +3 -3
- package/lib/manager.js +184 -66
- package/package.json +4 -1
- package/server/admin/access.js +12 -7
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +436 -10
- package/server/admin/billing.js +165 -42
- package/server/admin/index.html +80 -5
- package/server/admin/users.js +15 -15
- package/server/db/database.js +125 -20
- package/server/http/routes.js +1 -0
- package/server/http/static.js +4 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.js.symbols +8475 -8467
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +91077 -87037
- package/server/routes/account.js +53 -0
- package/server/routes/admin.js +345 -64
- package/server/routes/agents.js +203 -21
- package/server/routes/billing.js +5 -0
- package/server/routes/browser.js +8 -1
- package/server/routes/recordings.js +96 -6
- package/server/routes/screenHistory.js +140 -2
- package/server/routes/timeline.js +43 -0
- package/server/services/account/erasure.js +263 -0
- package/server/services/ai/hooks.js +4 -1
- package/server/services/ai/loop/agent_engine_core.js +8 -1
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/conversation_loop.js +166 -33
- package/server/services/ai/messagingFallback.js +22 -0
- package/server/services/ai/rate_limits.js +28 -5
- package/server/services/ai/systemPrompt.js +6 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/toolEvidence.js +15 -0
- package/server/services/ai/toolResult.js +40 -0
- package/server/services/ai/tools.js +163 -4
- package/server/services/android/controller.js +6 -2
- package/server/services/billing/plans.js +2 -1
- package/server/services/browser/anti_detection.js +192 -0
- package/server/services/browser/controller.js +180 -54
- package/server/services/desktop/auth.js +3 -0
- package/server/services/desktop/registry.js +50 -2
- package/server/services/integrations/google/calendar.js +22 -14
- package/server/services/manager.js +12 -42
- package/server/services/memory/ingestion_chunking.js +268 -0
- package/server/services/messaging/telnyx.js +9 -8
- package/server/services/recordings/manager.js +60 -27
- package/server/services/runtime/backends/local-vm.js +40 -22
- package/server/services/runtime/docker-vm-manager.js +157 -266
- package/server/services/runtime/guest_bootstrap.js +17 -5
- package/server/services/runtime/guest_image.js +188 -0
- package/server/services/runtime/manager.js +0 -1
- package/server/services/runtime/validation.js +3 -8
- package/server/services/social_video/service.js +60 -10
- package/server/services/tasks/runtime.js +234 -9
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/tasks/task_repository.js +13 -0
- package/server/services/timeline/service.js +558 -0
- package/server/services/wearable/gateway.js +1 -1
- package/server/services/websocket.js +21 -3
- package/server/services/desktop/screenRecorder.js +0 -292
- package/server/services/desktop/screen_recorder_support.js +0 -46
package/server/routes/account.js
CHANGED
|
@@ -328,4 +328,57 @@ router.delete('/providers/:id', accountLimiter, (req, res) => {
|
|
|
328
328
|
}
|
|
329
329
|
});
|
|
330
330
|
|
|
331
|
+
// GDPR Art. 20 — data portability. Returns a structured export of the
|
|
332
|
+
// authenticated user's own data (credential columns redacted).
|
|
333
|
+
router.get('/export', accountLimiter, (req, res) => {
|
|
334
|
+
try {
|
|
335
|
+
const { exportUserData } = require('../services/account/erasure');
|
|
336
|
+
const payload = exportUserData(req.session.userId);
|
|
337
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
338
|
+
res.setHeader(
|
|
339
|
+
'Content-Disposition',
|
|
340
|
+
`attachment; filename="neoagent-data-export-${req.session.userId}.json"`,
|
|
341
|
+
);
|
|
342
|
+
res.send(JSON.stringify(payload, null, 2));
|
|
343
|
+
} catch (err) {
|
|
344
|
+
sendRouteError(res, err);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// GDPR Art. 17 — right to erasure. The user permanently deletes their own
|
|
349
|
+
// account and all associated data. Requires typing the exact username as a
|
|
350
|
+
// destructive-action confirmation (works for password and SSO accounts alike).
|
|
351
|
+
router.post('/delete', accountLimiter, (req, res) => {
|
|
352
|
+
try {
|
|
353
|
+
const userId = req.session.userId;
|
|
354
|
+
const user = db
|
|
355
|
+
.prepare('SELECT username FROM users WHERE id = ?')
|
|
356
|
+
.get(userId);
|
|
357
|
+
if (!user) {
|
|
358
|
+
return res.status(404).json({ error: 'Account not found.' });
|
|
359
|
+
}
|
|
360
|
+
const confirm = String(req.body?.confirmUsername || '').trim();
|
|
361
|
+
if (confirm !== user.username) {
|
|
362
|
+
return res.status(400).json({
|
|
363
|
+
error: 'Confirmation does not match your username.',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const { eraseUserData } = require('../services/account/erasure');
|
|
368
|
+
const result = eraseUserData(userId, {
|
|
369
|
+
runtimeManager: req.app.locals.runtimeManager,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
req.session.destroy(() => {
|
|
373
|
+
res.clearCookie('neoagent.sid');
|
|
374
|
+
res.json({ ok: true, tablesCleared: result.tablesCleared });
|
|
375
|
+
});
|
|
376
|
+
} catch (err) {
|
|
377
|
+
if (err.code === 'NOT_FOUND') {
|
|
378
|
+
return res.status(404).json({ error: 'Account not found.' });
|
|
379
|
+
}
|
|
380
|
+
sendRouteError(res, err);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
331
384
|
module.exports = router;
|
package/server/routes/admin.js
CHANGED
|
@@ -563,59 +563,16 @@ router.get('/api/users', requireAdminAuth, (req, res) => {
|
|
|
563
563
|
});
|
|
564
564
|
|
|
565
565
|
router.delete('/api/users/:id', requireAdminAuth, (req, res) => {
|
|
566
|
-
const
|
|
567
|
-
const { DATA_DIR } = require('../../runtime/paths');
|
|
566
|
+
const { eraseUserData } = require('../services/account/erasure');
|
|
568
567
|
const { id } = req.params;
|
|
569
568
|
if (!id) return res.status(400).json({ error: 'Missing user id' });
|
|
570
569
|
if (!/^\d+$/.test(id)) return res.status(400).json({ error: 'Invalid user id' });
|
|
571
570
|
try {
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
// Collect artifact paths before deletion
|
|
576
|
-
const artifacts = db.prepare('SELECT storage_path FROM artifacts WHERE user_id = ?').all(id);
|
|
577
|
-
|
|
578
|
-
const erase = db.transaction((uid) => {
|
|
579
|
-
db.prepare('DELETE FROM conversation_messages WHERE conversation_id IN (SELECT id FROM conversations WHERE user_id = ?)').run(uid);
|
|
580
|
-
db.prepare('DELETE FROM conversations WHERE user_id = ?').run(uid);
|
|
581
|
-
db.prepare('DELETE FROM agent_steps WHERE run_id IN (SELECT id FROM agent_runs WHERE user_id = ?)').run(uid);
|
|
582
|
-
db.prepare('DELETE FROM agent_runs WHERE user_id = ?').run(uid);
|
|
583
|
-
db.prepare('DELETE FROM messages WHERE user_id = ?').run(uid);
|
|
584
|
-
db.prepare('DELETE FROM memories WHERE user_id = ?').run(uid);
|
|
585
|
-
db.prepare('DELETE FROM integration_connections WHERE user_id = ?').run(uid);
|
|
586
|
-
db.prepare('DELETE FROM platform_connections WHERE user_id = ?').run(uid);
|
|
587
|
-
db.prepare('DELETE FROM mcp_servers WHERE user_id = ?').run(uid);
|
|
588
|
-
db.prepare('DELETE FROM user_settings WHERE user_id = ?').run(uid);
|
|
589
|
-
db.prepare('DELETE FROM user_sessions WHERE user_id = ?').run(uid);
|
|
590
|
-
db.prepare('DELETE FROM desktop_companion_devices WHERE user_id = ?').run(uid);
|
|
591
|
-
db.prepare('DELETE FROM scheduled_tasks WHERE user_id = ?').run(uid);
|
|
592
|
-
db.prepare('DELETE FROM recording_sessions WHERE user_id = ?').run(uid);
|
|
593
|
-
db.prepare('DELETE FROM screen_history WHERE user_id = ?').run(uid);
|
|
594
|
-
db.prepare('DELETE FROM agents WHERE user_id = ?').run(uid);
|
|
595
|
-
db.prepare('DELETE FROM artifacts WHERE user_id = ?').run(uid);
|
|
596
|
-
db.prepare('DELETE FROM users WHERE id = ?').run(uid);
|
|
597
|
-
});
|
|
598
|
-
erase(id);
|
|
599
|
-
|
|
600
|
-
// Clean up artifact files on disk — containment-checked
|
|
601
|
-
const artifactsRoot = path.resolve(path.join(DATA_DIR, 'artifacts'));
|
|
602
|
-
for (const artifact of artifacts) {
|
|
603
|
-
try {
|
|
604
|
-
const abs = path.resolve(path.isAbsolute(artifact.storage_path)
|
|
605
|
-
? artifact.storage_path
|
|
606
|
-
: path.join(DATA_DIR, artifact.storage_path));
|
|
607
|
-
if (abs.startsWith(artifactsRoot + path.sep)) {
|
|
608
|
-
fs.rmSync(abs, { force: true });
|
|
609
|
-
}
|
|
610
|
-
} catch {}
|
|
611
|
-
}
|
|
612
|
-
const userArtifactDir = path.resolve(path.join(DATA_DIR, 'artifacts', id));
|
|
613
|
-
if (userArtifactDir.startsWith(artifactsRoot + path.sep)) {
|
|
614
|
-
try { fs.rmSync(userArtifactDir, { recursive: true, force: true }); } catch {}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
res.json({ ok: true });
|
|
571
|
+
const result = eraseUserData(id, { runtimeManager: req.app.locals.runtimeManager });
|
|
572
|
+
res.json(result);
|
|
618
573
|
} catch (err) {
|
|
574
|
+
if (err.code === 'NOT_FOUND') return res.status(404).json({ error: 'User not found' });
|
|
575
|
+
if (err.code === 'INVALID_ID') return res.status(400).json({ error: 'Invalid user id' });
|
|
619
576
|
res.status(500).json({ error: String(err.message || err) });
|
|
620
577
|
}
|
|
621
578
|
});
|
|
@@ -643,10 +600,15 @@ router.post('/api/sql', requireAdminAuth, sqlLimiter, express.json(), (req, res)
|
|
|
643
600
|
if (SQL_BLOCKED.test(trimmed)) return res.status(400).json({ error: 'Query contains a blocked SQL keyword' });
|
|
644
601
|
try {
|
|
645
602
|
const db = require('../db/database');
|
|
646
|
-
const
|
|
647
|
-
const
|
|
603
|
+
const limited = [];
|
|
604
|
+
for (const row of db.prepare(trimmed).iterate()) {
|
|
605
|
+
limited.push(row);
|
|
606
|
+
if (limited.length > 500) break;
|
|
607
|
+
}
|
|
608
|
+
const truncated = limited.length > 500;
|
|
609
|
+
if (truncated) limited.pop();
|
|
648
610
|
const columns = limited.length ? Object.keys(limited[0]) : [];
|
|
649
|
-
res.json({ rows: limited, columns, truncated:
|
|
611
|
+
res.json({ rows: limited, columns, truncated, total: truncated ? null : limited.length });
|
|
650
612
|
} catch (err) {
|
|
651
613
|
res.status(400).json({ error: String(err.message || err) });
|
|
652
614
|
}
|
|
@@ -655,16 +617,22 @@ router.post('/api/sql', requireAdminAuth, sqlLimiter, express.json(), (req, res)
|
|
|
655
617
|
// --- Providers ---
|
|
656
618
|
|
|
657
619
|
const PROVIDERS = [
|
|
658
|
-
{ key: 'ANTHROPIC_API_KEY',
|
|
659
|
-
{ key: 'OPENAI_API_KEY',
|
|
660
|
-
{ key: 'XAI_API_KEY',
|
|
661
|
-
{ key: 'GOOGLE_AI_KEY',
|
|
662
|
-
{ key: 'MINIMAX_API_KEY',
|
|
663
|
-
{ key: 'NVIDIA_API_KEY',
|
|
664
|
-
{ key: 'OPENROUTER_API_KEY',
|
|
665
|
-
{ key: 'BRAVE_SEARCH_API_KEY',
|
|
666
|
-
{ key: 'DEEPGRAM_API_KEY',
|
|
667
|
-
{ key: '
|
|
620
|
+
{ key: 'ANTHROPIC_API_KEY', label: 'Anthropic (Claude)', type: 'key' },
|
|
621
|
+
{ key: 'OPENAI_API_KEY', label: 'OpenAI', type: 'key' },
|
|
622
|
+
{ key: 'XAI_API_KEY', label: 'xAI (Grok)', type: 'key' },
|
|
623
|
+
{ key: 'GOOGLE_AI_KEY', label: 'Google (Gemini)', type: 'key' },
|
|
624
|
+
{ key: 'MINIMAX_API_KEY', label: 'MiniMax', type: 'key' },
|
|
625
|
+
{ key: 'NVIDIA_API_KEY', label: 'NVIDIA NIM', type: 'key' },
|
|
626
|
+
{ key: 'OPENROUTER_API_KEY', label: 'OpenRouter', type: 'key' },
|
|
627
|
+
{ key: 'BRAVE_SEARCH_API_KEY', label: 'Brave Search', type: 'key' },
|
|
628
|
+
{ key: 'DEEPGRAM_API_KEY', label: 'Deepgram (Voice)', type: 'key' },
|
|
629
|
+
{ key: 'GITHUB_COPILOT_ACCESS_TOKEN', label: 'GitHub Copilot', type: 'key' },
|
|
630
|
+
{ key: 'OPENAI_CODEX_ACCESS_TOKEN', label: 'OpenAI Codex', type: 'key' },
|
|
631
|
+
{ key: 'TELNYX_WEBHOOK_TOKEN', label: 'Telnyx Webhook Token', type: 'key' },
|
|
632
|
+
{ key: 'OLLAMA_URL', label: 'Ollama (Local)', type: 'url' },
|
|
633
|
+
{ key: 'OPENAI_BASE_URL', label: 'OpenAI Base URL override', type: 'url' },
|
|
634
|
+
{ key: 'ANTHROPIC_BASE_URL', label: 'Anthropic Base URL override', type: 'url' },
|
|
635
|
+
{ key: 'XAI_BASE_URL', label: 'xAI Base URL override', type: 'url' },
|
|
668
636
|
];
|
|
669
637
|
|
|
670
638
|
const ALLOWED_PROVIDER_KEYS = new Set(PROVIDERS.map((p) => p.key));
|
|
@@ -700,6 +668,306 @@ router.put('/api/providers', requireAdminAuth, express.json(), (req, res) => {
|
|
|
700
668
|
res.json({ ok: true });
|
|
701
669
|
});
|
|
702
670
|
|
|
671
|
+
// --- General server config ---
|
|
672
|
+
|
|
673
|
+
function parseEnvBool(key, defaultVal) {
|
|
674
|
+
const v = (process.env[key] || '').toLowerCase();
|
|
675
|
+
return v ? ['1', 'true', 'yes', 'on'].includes(v) : defaultVal;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function parseEnvInt(key, defaultVal) {
|
|
679
|
+
const n = parseInt(process.env[key] || '', 10);
|
|
680
|
+
return Number.isFinite(n) ? n : defaultVal;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function cleanLine(v) {
|
|
684
|
+
return String(v ?? '').trim().replace(/[\r\n]/g, '');
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function persistEnv(key, value) {
|
|
688
|
+
const strVal = String(value);
|
|
689
|
+
upsertEnvValue(ENV_FILE, key, strVal);
|
|
690
|
+
if (strVal === '') {
|
|
691
|
+
delete process.env[key];
|
|
692
|
+
} else {
|
|
693
|
+
process.env[key] = strVal;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
router.get('/api/config/general', requireAdminAuth, (req, res) => {
|
|
698
|
+
res.json({
|
|
699
|
+
settings: {
|
|
700
|
+
publicUrl: process.env.PUBLIC_URL || '',
|
|
701
|
+
secureCookies: parseEnvBool('SECURE_COOKIES', false),
|
|
702
|
+
neoagentProfile: process.env.NEOAGENT_PROFILE || '',
|
|
703
|
+
allowedOrigins: process.env.ALLOWED_ORIGINS || '',
|
|
704
|
+
meshtasticEnabled: parseEnvBool('MESHTASTIC_ENABLED', true),
|
|
705
|
+
memoryIngestionIntervalMs: parseEnvInt('NEOAGENT_MEMORY_INGESTION_INTERVAL_MS', 600000),
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
router.put('/api/config/general', requireAdminAuth, settingsLimiter, express.json(), (req, res) => {
|
|
711
|
+
try {
|
|
712
|
+
const b = req.body || {};
|
|
713
|
+
const publicUrl = cleanLine(b.publicUrl);
|
|
714
|
+
if (publicUrl) {
|
|
715
|
+
try { new URL(publicUrl); } catch {
|
|
716
|
+
return res.status(400).json({ error: 'publicUrl must be a valid URL.' });
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const profile = cleanLine(b.neoagentProfile);
|
|
720
|
+
if (profile && !['prod', 'private'].includes(profile)) {
|
|
721
|
+
return res.status(400).json({ error: 'neoagentProfile must be "prod" or "private".' });
|
|
722
|
+
}
|
|
723
|
+
const allowedOrigins = cleanLine(b.allowedOrigins);
|
|
724
|
+
const intervalMs = parseInt(b.memoryIngestionIntervalMs, 10);
|
|
725
|
+
if (!Number.isFinite(intervalMs) || intervalMs < 1000) {
|
|
726
|
+
return res.status(400).json({ error: 'memoryIngestionIntervalMs must be ≥ 1000.' });
|
|
727
|
+
}
|
|
728
|
+
if (typeof b.secureCookies !== 'boolean') {
|
|
729
|
+
return res.status(400).json({ error: 'secureCookies must be a boolean.' });
|
|
730
|
+
}
|
|
731
|
+
if (typeof b.meshtasticEnabled !== 'boolean') {
|
|
732
|
+
return res.status(400).json({ error: 'meshtasticEnabled must be a boolean.' });
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
persistEnv('PUBLIC_URL', publicUrl);
|
|
736
|
+
persistEnv('SECURE_COOKIES', b.secureCookies ? 'true' : 'false');
|
|
737
|
+
persistEnv('NEOAGENT_PROFILE', profile);
|
|
738
|
+
persistEnv('ALLOWED_ORIGINS', allowedOrigins);
|
|
739
|
+
persistEnv('MESHTASTIC_ENABLED', b.meshtasticEnabled ? 'true' : 'false');
|
|
740
|
+
persistEnv('NEOAGENT_MEMORY_INGESTION_INTERVAL_MS', String(intervalMs));
|
|
741
|
+
|
|
742
|
+
res.json({ ok: true });
|
|
743
|
+
} catch (err) {
|
|
744
|
+
res.status(500).json({ error: err.message });
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
// --- VM runtime config ---
|
|
749
|
+
|
|
750
|
+
router.get('/api/config/vm', requireAdminAuth, (req, res) => {
|
|
751
|
+
res.json({
|
|
752
|
+
settings: {
|
|
753
|
+
vmBaseImageUrl: process.env.NEOAGENT_VM_BASE_IMAGE_URL || '',
|
|
754
|
+
vmBaseImage: process.env.NEOAGENT_VM_BASE_IMAGE || '',
|
|
755
|
+
vmMemoryMb: parseEnvInt('NEOAGENT_VM_MEMORY_MB', 4096),
|
|
756
|
+
vmCpus: parseEnvInt('NEOAGENT_VM_CPUS', 2),
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
router.put('/api/config/vm', requireAdminAuth, settingsLimiter, express.json(), (req, res) => {
|
|
762
|
+
try {
|
|
763
|
+
const b = req.body || {};
|
|
764
|
+
const vmBaseImageUrl = cleanLine(b.vmBaseImageUrl);
|
|
765
|
+
const vmBaseImage = cleanLine(b.vmBaseImage);
|
|
766
|
+
const vmMemoryMb = parseInt(b.vmMemoryMb, 10);
|
|
767
|
+
const vmCpus = parseInt(b.vmCpus, 10);
|
|
768
|
+
|
|
769
|
+
if (vmBaseImageUrl) {
|
|
770
|
+
try { new URL(vmBaseImageUrl); } catch {
|
|
771
|
+
return res.status(400).json({ error: 'vmBaseImageUrl must be a valid URL.' });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (!Number.isFinite(vmMemoryMb) || vmMemoryMb < 512) {
|
|
775
|
+
return res.status(400).json({ error: 'vmMemoryMb must be ≥ 512.' });
|
|
776
|
+
}
|
|
777
|
+
if (!Number.isFinite(vmCpus) || vmCpus < 1) {
|
|
778
|
+
return res.status(400).json({ error: 'vmCpus must be ≥ 1.' });
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
persistEnv('NEOAGENT_VM_BASE_IMAGE_URL', vmBaseImageUrl);
|
|
782
|
+
persistEnv('NEOAGENT_VM_BASE_IMAGE', vmBaseImage);
|
|
783
|
+
persistEnv('NEOAGENT_VM_MEMORY_MB', String(vmMemoryMb));
|
|
784
|
+
persistEnv('NEOAGENT_VM_CPUS', String(vmCpus));
|
|
785
|
+
|
|
786
|
+
res.json({ ok: true });
|
|
787
|
+
} catch (err) {
|
|
788
|
+
res.status(500).json({ error: err.message });
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// --- OAuth integrations config ---
|
|
793
|
+
|
|
794
|
+
const OAUTH_INTEGRATIONS = [
|
|
795
|
+
{
|
|
796
|
+
key: 'google',
|
|
797
|
+
label: 'Google Workspace',
|
|
798
|
+
fields: [
|
|
799
|
+
{ name: 'clientId', env: 'GOOGLE_OAUTH_CLIENT_ID', secret: false },
|
|
800
|
+
{ name: 'clientSecret', env: 'GOOGLE_OAUTH_CLIENT_SECRET', secret: true },
|
|
801
|
+
{ name: 'redirectUri', env: 'GOOGLE_OAUTH_REDIRECT_URI', secret: false },
|
|
802
|
+
],
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
key: 'notion',
|
|
806
|
+
label: 'Notion',
|
|
807
|
+
fields: [
|
|
808
|
+
{ name: 'clientId', env: 'NOTION_OAUTH_CLIENT_ID', secret: false },
|
|
809
|
+
{ name: 'clientSecret', env: 'NOTION_OAUTH_CLIENT_SECRET', secret: true },
|
|
810
|
+
{ name: 'redirectUri', env: 'NOTION_OAUTH_REDIRECT_URI', secret: false },
|
|
811
|
+
],
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
key: 'microsoft',
|
|
815
|
+
label: 'Microsoft 365',
|
|
816
|
+
fields: [
|
|
817
|
+
{ name: 'clientId', env: 'MICROSOFT_OAUTH_CLIENT_ID', secret: false },
|
|
818
|
+
{ name: 'clientSecret', env: 'MICROSOFT_OAUTH_CLIENT_SECRET', secret: true },
|
|
819
|
+
{ name: 'redirectUri', env: 'MICROSOFT_OAUTH_REDIRECT_URI', secret: false },
|
|
820
|
+
{ name: 'tenantId', env: 'MICROSOFT_OAUTH_TENANT_ID', secret: false },
|
|
821
|
+
],
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
key: 'slack',
|
|
825
|
+
label: 'Slack',
|
|
826
|
+
fields: [
|
|
827
|
+
{ name: 'clientId', env: 'SLACK_OAUTH_CLIENT_ID', secret: false },
|
|
828
|
+
{ name: 'clientSecret', env: 'SLACK_OAUTH_CLIENT_SECRET', secret: true },
|
|
829
|
+
{ name: 'redirectUri', env: 'SLACK_OAUTH_REDIRECT_URI', secret: false },
|
|
830
|
+
],
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
key: 'figma',
|
|
834
|
+
label: 'Figma',
|
|
835
|
+
fields: [
|
|
836
|
+
{ name: 'clientId', env: 'FIGMA_OAUTH_CLIENT_ID', secret: false },
|
|
837
|
+
{ name: 'clientSecret', env: 'FIGMA_OAUTH_CLIENT_SECRET', secret: true },
|
|
838
|
+
{ name: 'redirectUri', env: 'FIGMA_OAUTH_REDIRECT_URI', secret: false },
|
|
839
|
+
],
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
key: 'github',
|
|
843
|
+
label: 'GitHub',
|
|
844
|
+
fields: [
|
|
845
|
+
{ name: 'clientId', env: 'GITHUB_OAUTH_CLIENT_ID', secret: false },
|
|
846
|
+
{ name: 'clientSecret', env: 'GITHUB_OAUTH_CLIENT_SECRET', secret: true },
|
|
847
|
+
{ name: 'redirectUri', env: 'GITHUB_OAUTH_REDIRECT_URI', secret: false },
|
|
848
|
+
],
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
key: 'spotify',
|
|
852
|
+
label: 'Spotify',
|
|
853
|
+
fields: [
|
|
854
|
+
{ name: 'clientId', env: 'SPOTIFY_OAUTH_CLIENT_ID', secret: false },
|
|
855
|
+
{ name: 'clientSecret', env: 'SPOTIFY_OAUTH_CLIENT_SECRET', secret: true },
|
|
856
|
+
{ name: 'redirectUri', env: 'SPOTIFY_OAUTH_REDIRECT_URI', secret: false },
|
|
857
|
+
],
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
key: 'trello',
|
|
861
|
+
label: 'Trello',
|
|
862
|
+
fields: [
|
|
863
|
+
{ name: 'apiKey', env: 'TRELLO_API_KEY', secret: false },
|
|
864
|
+
],
|
|
865
|
+
},
|
|
866
|
+
];
|
|
867
|
+
|
|
868
|
+
const OAUTH_ENV_KEYS = new Set(
|
|
869
|
+
OAUTH_INTEGRATIONS.flatMap((i) => i.fields.map((f) => f.env))
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
router.get('/api/config/integrations', requireAdminAuth, (req, res) => {
|
|
873
|
+
const integrations = OAUTH_INTEGRATIONS.map(({ key, label, fields }) => {
|
|
874
|
+
const fieldData = fields.map(({ name, env, secret }) => {
|
|
875
|
+
const value = process.env[env] || '';
|
|
876
|
+
if (secret) {
|
|
877
|
+
return { name, secret: true, configured: Boolean(value) };
|
|
878
|
+
}
|
|
879
|
+
return { name, secret: false, value };
|
|
880
|
+
});
|
|
881
|
+
const configured = fields.every(({ env }) => Boolean(process.env[env]));
|
|
882
|
+
return { key, label, configured, fields: fieldData };
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// Deepgram service settings (not secret, grouped here)
|
|
886
|
+
const deepgram = {
|
|
887
|
+
baseUrl: process.env.DEEPGRAM_BASE_URL || '',
|
|
888
|
+
model: process.env.DEEPGRAM_MODEL || '',
|
|
889
|
+
language: process.env.DEEPGRAM_LANGUAGE || '',
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
res.json({ integrations, deepgram });
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
router.put('/api/config/integrations', requireAdminAuth, settingsLimiter, express.json(), (req, res) => {
|
|
896
|
+
try {
|
|
897
|
+
const b = req.body || {};
|
|
898
|
+
|
|
899
|
+
// OAuth integrations
|
|
900
|
+
const patches = b.integrations || {};
|
|
901
|
+
for (const [intKey, fieldValues] of Object.entries(patches)) {
|
|
902
|
+
const integration = OAUTH_INTEGRATIONS.find((i) => i.key === intKey);
|
|
903
|
+
if (!integration) continue;
|
|
904
|
+
for (const [fieldName, value] of Object.entries(fieldValues)) {
|
|
905
|
+
const fieldDef = integration.fields.find((f) => f.name === fieldName);
|
|
906
|
+
if (!fieldDef) continue;
|
|
907
|
+
const cleaned = cleanLine(value);
|
|
908
|
+
if (fieldDef.secret && cleaned === '') continue; // blank = no-change for secrets
|
|
909
|
+
persistEnv(fieldDef.env, cleaned);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Deepgram config
|
|
914
|
+
if (b.deepgram) {
|
|
915
|
+
persistEnv('DEEPGRAM_BASE_URL', cleanLine(b.deepgram.baseUrl));
|
|
916
|
+
persistEnv('DEEPGRAM_MODEL', cleanLine(b.deepgram.model));
|
|
917
|
+
persistEnv('DEEPGRAM_LANGUAGE', cleanLine(b.deepgram.language));
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
res.json({ ok: true });
|
|
921
|
+
} catch (err) {
|
|
922
|
+
res.status(500).json({ error: err.message });
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// --- Stripe / billing setup config ---
|
|
927
|
+
|
|
928
|
+
router.get('/api/config/billing-setup', requireAdminAuth, (req, res) => {
|
|
929
|
+
const secretKey = process.env.STRIPE_SECRET_KEY || '';
|
|
930
|
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || '';
|
|
931
|
+
res.json({
|
|
932
|
+
settings: {
|
|
933
|
+
billingEnabled: parseEnvBool('NEOAGENT_BILLING_ENABLED', false),
|
|
934
|
+
stripePublishableKey: process.env.STRIPE_PUBLISHABLE_KEY || '',
|
|
935
|
+
stripeSecretKeyConfigured: Boolean(secretKey),
|
|
936
|
+
stripeSecretKeyHint: secretKey.length > 8
|
|
937
|
+
? `${secretKey.slice(0, 7)}${'•'.repeat(4)}${secretKey.slice(-4)}`
|
|
938
|
+
: secretKey ? '•'.repeat(secretKey.length) : '',
|
|
939
|
+
stripeWebhookSecretConfigured: Boolean(webhookSecret),
|
|
940
|
+
trialDays: parseEnvInt('BILLING_TRIAL_DAYS', 14),
|
|
941
|
+
},
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
router.put('/api/config/billing-setup', requireAdminAuth, settingsLimiter, express.json(), (req, res) => {
|
|
946
|
+
try {
|
|
947
|
+
const b = req.body || {};
|
|
948
|
+
if (typeof b.billingEnabled !== 'boolean') {
|
|
949
|
+
return res.status(400).json({ error: 'billingEnabled must be a boolean.' });
|
|
950
|
+
}
|
|
951
|
+
const publishableKey = cleanLine(b.stripePublishableKey);
|
|
952
|
+
const secretKey = cleanLine(b.stripeSecretKey || '');
|
|
953
|
+
const webhookSecret = cleanLine(b.stripeWebhookSecret || '');
|
|
954
|
+
const trialDays = parseInt(b.trialDays, 10);
|
|
955
|
+
if (!Number.isFinite(trialDays) || trialDays < 0) {
|
|
956
|
+
return res.status(400).json({ error: 'trialDays must be ≥ 0.' });
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
persistEnv('NEOAGENT_BILLING_ENABLED', b.billingEnabled ? 'true' : 'false');
|
|
960
|
+
persistEnv('STRIPE_PUBLISHABLE_KEY', publishableKey);
|
|
961
|
+
if (secretKey) persistEnv('STRIPE_SECRET_KEY', secretKey);
|
|
962
|
+
if (webhookSecret) persistEnv('STRIPE_WEBHOOK_SECRET', webhookSecret);
|
|
963
|
+
persistEnv('BILLING_TRIAL_DAYS', String(trialDays));
|
|
964
|
+
|
|
965
|
+
res.json({ ok: true });
|
|
966
|
+
} catch (err) {
|
|
967
|
+
res.status(500).json({ error: err.message });
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
|
|
703
971
|
// --- Global default rate limits ---
|
|
704
972
|
|
|
705
973
|
router.get('/api/config/rate-limits', requireAdminAuth, (req, res) => {
|
|
@@ -853,8 +1121,10 @@ router.put('/api/users/:id/rate-limits', requireAdminAuth, express.json(), (req,
|
|
|
853
1121
|
|
|
854
1122
|
// Per-user subscription management
|
|
855
1123
|
router.get('/api/billing/users/:id/subscription', requireAdminAuth, (req, res) => {
|
|
1124
|
+
const userId = parseInt(req.params.id, 10);
|
|
1125
|
+
if (isNaN(userId)) return res.status(400).json({ error: 'Invalid user id.' });
|
|
856
1126
|
try {
|
|
857
|
-
const sub = billingSubscriptions.getActiveSubscription(
|
|
1127
|
+
const sub = billingSubscriptions.getActiveSubscription(userId);
|
|
858
1128
|
res.json({ subscription: sub });
|
|
859
1129
|
} catch (err) {
|
|
860
1130
|
res.status(500).json({ error: err.message });
|
|
@@ -862,8 +1132,9 @@ router.put('/api/users/:id/rate-limits', requireAdminAuth, express.json(), (req,
|
|
|
862
1132
|
});
|
|
863
1133
|
|
|
864
1134
|
router.post('/api/billing/users/:id/subscription', requireAdminAuth, express.json(), (req, res) => {
|
|
1135
|
+
const userId = parseInt(req.params.id, 10);
|
|
1136
|
+
if (isNaN(userId)) return res.status(400).json({ error: 'Invalid user id.' });
|
|
865
1137
|
try {
|
|
866
|
-
const userId = parseInt(req.params.id, 10);
|
|
867
1138
|
const { planId, status } = req.body;
|
|
868
1139
|
if (!planId) return res.status(400).json({ error: 'planId is required.' });
|
|
869
1140
|
const sub = billingSubscriptions.adminSetSubscription(userId, planId, status);
|
|
@@ -874,8 +1145,10 @@ router.put('/api/users/:id/rate-limits', requireAdminAuth, express.json(), (req,
|
|
|
874
1145
|
});
|
|
875
1146
|
|
|
876
1147
|
router.delete('/api/billing/users/:id/subscription', requireAdminAuth, (req, res) => {
|
|
1148
|
+
const userId = parseInt(req.params.id, 10);
|
|
1149
|
+
if (isNaN(userId)) return res.status(400).json({ error: 'Invalid user id.' });
|
|
877
1150
|
try {
|
|
878
|
-
billingSubscriptions.adminCancelSubscription(
|
|
1151
|
+
billingSubscriptions.adminCancelSubscription(userId);
|
|
879
1152
|
res.json({ ok: true });
|
|
880
1153
|
} catch (err) {
|
|
881
1154
|
res.status(500).json({ error: err.message });
|
|
@@ -883,6 +1156,14 @@ router.put('/api/users/:id/rate-limits', requireAdminAuth, express.json(), (req,
|
|
|
883
1156
|
});
|
|
884
1157
|
})();
|
|
885
1158
|
|
|
1159
|
+
// --- API 404 guard (must come before static files) ---
|
|
1160
|
+
// Prevents unregistered /api/* routes (e.g. billing when disabled) from
|
|
1161
|
+
// falling through to the HTML catch-all and returning a false 200.
|
|
1162
|
+
|
|
1163
|
+
router.all('/api/*', (req, res) => {
|
|
1164
|
+
res.status(404).json({ error: 'Not found.' });
|
|
1165
|
+
});
|
|
1166
|
+
|
|
886
1167
|
// --- Static files ---
|
|
887
1168
|
|
|
888
1169
|
router.use(express.static(ADMIN_DIR));
|