neoagent 2.4.1-beta.8 → 2.4.1

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 (248) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +45 -7
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -2
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +15 -2
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +3 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +338 -46
  52. package/flutter_app/lib/main_devices.dart +851 -122
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +764 -887
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +79 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
  70. package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
  71. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  72. package/flutter_app/lib/src/stream_renderer.dart +205 -35
  73. package/flutter_app/lib/src/theme/palette.dart +76 -34
  74. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  75. package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  81. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  82. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  83. package/flutter_app/pubspec.lock +2 -2
  84. package/flutter_app/pubspec.yaml +7 -1
  85. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  94. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  95. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  96. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  97. package/flutter_app/web/favicon.png +0 -0
  98. package/flutter_app/web/favicon.svg +40 -9
  99. package/flutter_app/web/favicon_light.svg +43 -0
  100. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-192.png +0 -0
  102. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-512.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  106. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  107. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  108. package/flutter_app/windows/runner/main.cpp +7 -1
  109. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  110. package/lib/manager.js +445 -83
  111. package/package.json +17 -3
  112. package/runtime/paths.js +3 -1
  113. package/server/admin/access.js +198 -0
  114. package/server/admin/admin.css +268 -0
  115. package/server/admin/admin.js +348 -0
  116. package/server/admin/analytics.js +128 -0
  117. package/server/admin/index.html +1015 -0
  118. package/server/admin/login.html +290 -0
  119. package/server/admin/logo.svg +43 -0
  120. package/server/admin/sql.js +134 -0
  121. package/server/admin/users.js +147 -0
  122. package/server/config/origins.js +3 -1
  123. package/server/db/database.js +92 -0
  124. package/server/db/ftsQuery.js +27 -0
  125. package/server/http/routes.js +1 -0
  126. package/server/http/static.js +23 -6
  127. package/server/index.js +1 -1
  128. package/server/middleware/adminAuth.js +48 -0
  129. package/server/middleware/auth.js +1 -40
  130. package/server/public/.last_build_id +1 -1
  131. package/server/public/app_icon.png +0 -0
  132. package/server/public/assets/AssetManifest.bin +1 -1
  133. package/server/public/assets/AssetManifest.bin.json +1 -1
  134. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  138. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  139. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  140. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  141. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  142. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  143. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  144. package/server/public/assets/web/icons/Icon-192.png +0 -0
  145. package/server/public/favicon.png +0 -0
  146. package/server/public/favicon.svg +40 -9
  147. package/server/public/favicon_light.svg +43 -0
  148. package/server/public/flutter_bootstrap.js +2 -2
  149. package/server/public/icons/Icon-192-light.png +0 -0
  150. package/server/public/icons/Icon-192.png +0 -0
  151. package/server/public/icons/Icon-512-light.png +0 -0
  152. package/server/public/icons/Icon-512.png +0 -0
  153. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-192.png +0 -0
  155. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  156. package/server/public/icons/Icon-maskable-512.png +0 -0
  157. package/server/public/main.dart.js +88944 -85720
  158. package/server/routes/admin.js +632 -0
  159. package/server/routes/agent_profiles.js +3 -0
  160. package/server/routes/agents.js +1 -1
  161. package/server/routes/auth.js +3 -1
  162. package/server/routes/browser.js +14 -0
  163. package/server/routes/browser_extension.js +21 -4
  164. package/server/routes/desktop.js +10 -0
  165. package/server/routes/mcp.js +29 -13
  166. package/server/routes/memory.js +23 -1
  167. package/server/routes/messaging.js +2 -0
  168. package/server/routes/screenHistory.js +14 -8
  169. package/server/routes/settings.js +14 -2
  170. package/server/routes/stream.js +12 -3
  171. package/server/routes/triggers.js +4 -4
  172. package/server/routes/voice_assistant.js +36 -1
  173. package/server/routes/workspace.js +86 -0
  174. package/server/services/account/admin_two_factor.js +132 -0
  175. package/server/services/account/password_policy.js +6 -1
  176. package/server/services/ai/completion.js +44 -0
  177. package/server/services/ai/engine.js +321 -500
  178. package/server/services/ai/imageAnalysis.js +9 -5
  179. package/server/services/ai/logFormat.js +46 -0
  180. package/server/services/ai/loopPolicy.js +11 -0
  181. package/server/services/ai/messagingFallback.js +228 -0
  182. package/server/services/ai/models.js +194 -55
  183. package/server/services/ai/providerRetry.js +169 -0
  184. package/server/services/ai/providers/anthropic.js +15 -4
  185. package/server/services/ai/providers/google.js +38 -11
  186. package/server/services/ai/providers/grok.js +19 -68
  187. package/server/services/ai/providers/grokOauth.js +141 -0
  188. package/server/services/ai/providers/nvidia.js +154 -0
  189. package/server/services/ai/providers/ollama.js +76 -39
  190. package/server/services/ai/providers/openai.js +20 -31
  191. package/server/services/ai/providers/openaiCodex.js +6 -2
  192. package/server/services/ai/providers/openaiCompatible.js +70 -0
  193. package/server/services/ai/providers/openrouter.js +162 -0
  194. package/server/services/ai/settings.js +30 -0
  195. package/server/services/ai/systemPrompt.js +51 -29
  196. package/server/services/ai/taskAnalysis.js +60 -1
  197. package/server/services/ai/toolEvidence.js +207 -0
  198. package/server/services/ai/toolSelector.js +8 -1
  199. package/server/services/ai/tools.js +80 -15
  200. package/server/services/android/controller.js +36 -1
  201. package/server/services/browser/controller.js +18 -0
  202. package/server/services/browser/extension/manifest.js +33 -0
  203. package/server/services/browser/extension/provider.js +12 -6
  204. package/server/services/browser/extension/registry.js +229 -18
  205. package/server/services/desktop/protocol.js +1 -0
  206. package/server/services/desktop/provider.js +4 -0
  207. package/server/services/desktop/registry.js +104 -1
  208. package/server/services/desktop/screenRecorder.js +208 -98
  209. package/server/services/desktop/screen_recorder_support.js +46 -0
  210. package/server/services/integrations/google/provider.js +13 -0
  211. package/server/services/integrations/home_assistant/constants.js +88 -0
  212. package/server/services/integrations/home_assistant/network.js +207 -0
  213. package/server/services/integrations/home_assistant/provider.js +249 -0
  214. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  215. package/server/services/integrations/home_assistant/tools.js +101 -0
  216. package/server/services/integrations/manager.js +5 -0
  217. package/server/services/integrations/registry.js +2 -0
  218. package/server/services/integrations/trello/provider.js +8 -2
  219. package/server/services/manager.js +51 -47
  220. package/server/services/mcp/client.js +208 -268
  221. package/server/services/mcp/client_support.js +172 -0
  222. package/server/services/mcp/recovery.js +116 -0
  223. package/server/services/mcp/tool_operations.js +123 -0
  224. package/server/services/memory/ingestion.js +185 -370
  225. package/server/services/memory/ingestion_coverage.js +129 -0
  226. package/server/services/memory/ingestion_documents.js +123 -0
  227. package/server/services/memory/ingestion_support.js +171 -0
  228. package/server/services/memory/intelligence.js +181 -0
  229. package/server/services/memory/manager.js +476 -32
  230. package/server/services/messaging/automation.js +71 -134
  231. package/server/services/messaging/formatting_guides.js +26 -1
  232. package/server/services/messaging/inbound_queue.js +111 -0
  233. package/server/services/messaging/manager.js +25 -8
  234. package/server/services/messaging/typing_keepalive.js +110 -0
  235. package/server/services/runtime/manager.js +7 -3
  236. package/server/services/runtime/settings.js +17 -0
  237. package/server/services/streaming/android-stream.js +296 -32
  238. package/server/services/streaming/stream-hub.js +1 -1
  239. package/server/services/tasks/integration_runtime.js +4 -1
  240. package/server/services/tasks/runtime.js +415 -56
  241. package/server/services/tasks/task_repository.js +48 -6
  242. package/server/services/websocket.js +8 -2
  243. package/server/services/widgets/service.js +17 -1
  244. package/server/services/workspace/manager.js +116 -0
  245. package/server/utils/logger.js +44 -6
  246. package/server/utils/security.js +3 -0
  247. package/server/utils/version.js +29 -19
  248. package/server/services/memory/openhuman_uplift.test.js +0 -98
@@ -4,10 +4,10 @@ const rateLimit = require('express-rate-limit');
4
4
  const { requireAuth } = require('../middleware/auth');
5
5
  const { sanitizeError } = require('../utils/security');
6
6
  const { createZipFromDirectory } = require('../services/browser/extension/zip');
7
+ const { getExtensionManifest } = require('../services/browser/extension/manifest');
7
8
 
8
9
  const router = express.Router();
9
10
  const EXTENSION_DIR = path.join(__dirname, '..', '..', 'extensions', 'chrome-browser');
10
- const EXTENSION_MANIFEST = require('../../extensions/chrome-browser/manifest.json');
11
11
  const pairingLimiter = rateLimit({
12
12
  windowMs: 15 * 60 * 1000,
13
13
  max: 30,
@@ -55,10 +55,12 @@ router.post('/pairing/request', pairingLimiter, (req, res) => {
55
55
  });
56
56
 
57
57
  router.get('/latest', (req, res) => {
58
+ const manifest = getExtensionManifest();
58
59
  res.json({
59
- name: EXTENSION_MANIFEST.name,
60
- version: EXTENSION_MANIFEST.version,
61
- minimumChromeVersion: EXTENSION_MANIFEST.minimum_chrome_version,
60
+ name: manifest.name,
61
+ version: manifest.version,
62
+ versionName: manifest.version_name,
63
+ minimumChromeVersion: manifest.minimum_chrome_version,
62
64
  downloadUrl: `${baseUrlFor(req)}/api/browser-extension/download`,
63
65
  });
64
66
  });
@@ -123,6 +125,19 @@ router.get('/status', requireAuth, (req, res) => {
123
125
  }
124
126
  });
125
127
 
128
+ router.post('/select-token', requireAuth, (req, res) => {
129
+ try {
130
+ const registry = getRegistry(req);
131
+ const result = registry.setSelectedTokenId(req.session.userId, req.body?.tokenId);
132
+ res.json({
133
+ ...result,
134
+ status: registry.getStatus(req.session.userId),
135
+ });
136
+ } catch (err) {
137
+ res.status(err.status || 500).json({ error: sanitizeError(err) });
138
+ }
139
+ });
140
+
126
141
  router.post('/revoke', requireAuth, (req, res) => {
127
142
  try {
128
143
  res.json(getRegistry(req).revoke(req.session.userId, req.body?.tokenId || null));
@@ -133,8 +148,10 @@ router.post('/revoke', requireAuth, (req, res) => {
133
148
 
134
149
  router.get('/download', requireAuth, (req, res) => {
135
150
  try {
151
+ const manifest = getExtensionManifest();
136
152
  const zip = createZipFromDirectory(EXTENSION_DIR, {
137
153
  overrides: {
154
+ 'manifest.json': `${JSON.stringify(manifest, null, 2)}\n`,
138
155
  'config.mjs': extensionConfigFor(req),
139
156
  },
140
157
  });
@@ -163,6 +163,16 @@ router.post('/click', (req, res) =>
163
163
  },
164
164
  )));
165
165
 
166
+ router.post('/mouse-move', (req, res) =>
167
+ handleDesktopAction(req, res, (provider, request) => provider.mouseMove(
168
+ parseFiniteNumber(request.body?.x, 'x', { min: -100000, max: 100000 }),
169
+ parseFiniteNumber(request.body?.y, 'y', { min: -100000, max: 100000 }),
170
+ {
171
+ ...(request.body || {}),
172
+ deviceId: parseDeviceId(request.body?.deviceId),
173
+ },
174
+ )));
175
+
166
176
  router.post('/drag', (req, res) =>
167
177
  handleDesktopAction(req, res, (provider, request) => provider.drag({
168
178
  ...(request.body || {}),
@@ -6,6 +6,7 @@ const { sanitizeError } = require('../utils/security');
6
6
  const { validateRemoteMcpEndpoint } = require('../services/runtime/mcp');
7
7
  const { getAgentIdFromRequest, isMainAgent, resolveAgentId } = require('../services/agents/manager');
8
8
  const { resolvePublicBaseUrl } = require('../services/integrations/env');
9
+ const { consumeOAuthState } = require('../services/mcp/client_support');
9
10
 
10
11
  const MCP_OAUTH_STATE_RE = /^(\d+)::[a-f0-9]{32}$/;
11
12
 
@@ -45,8 +46,11 @@ router.get('/', (req, res) => {
45
46
  config: JSON.parse(s.config || '{}'),
46
47
  agentId: s.agent_id || null,
47
48
  enabled: !!s.enabled,
48
- status: liveStatuses[s.id]?.status || 'stopped',
49
- toolCount: liveStatuses[s.id]?.toolCount || 0
49
+ status: liveStatuses[s.id]?.status || s.status || 'stopped',
50
+ toolCount: liveStatuses[s.id]?.toolCount || 0,
51
+ error: liveStatuses[s.id]?.error || null,
52
+ consecutiveFails: liveStatuses[s.id]?.consecutiveFails || 0,
53
+ nextRetryAt: liveStatuses[s.id]?.nextRetryAt || null,
50
54
  }));
51
55
 
52
56
  res.json(result);
@@ -70,20 +74,26 @@ router.post('/', (req, res) => {
70
74
  });
71
75
 
72
76
  // Update an MCP server
73
- router.put('/:id', (req, res) => {
77
+ router.put('/:id', async (req, res) => {
74
78
  try {
75
79
  const server = db.prepare('SELECT * FROM mcp_servers WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
76
80
  if (!server) return res.status(404).json({ error: 'Server not found' });
77
81
 
82
+ const mcpClient = req.app.locals.mcpClient;
83
+ const wasLive = Boolean(mcpClient.getStatus(req.session.userId)[server.id]);
78
84
  const { name, command, config, enabled } = req.body;
79
85
  const agentId = (req.body.agentId !== undefined || req.body.agent_id !== undefined)
80
86
  ? resolveAgentId(req.session.userId, getAgentIdFromRequest(req))
81
87
  : (server.agent_id || resolveAgentId(req.session.userId, null));
82
88
  const endpoint = command ? validateRemoteMcpEndpoint(command) : server.command;
83
- db.prepare('UPDATE mcp_servers SET agent_id = ?, name = ?, command = ?, config = ?, enabled = ? WHERE id = ?')
89
+ db.prepare("UPDATE mcp_servers SET agent_id = ?, name = ?, command = ?, config = ?, enabled = ?, status = 'stopped' WHERE id = ?")
84
90
  .run(agentId, name || server.name, endpoint, JSON.stringify(config || JSON.parse(server.config)), enabled !== undefined ? (enabled ? 1 : 0) : server.enabled, server.id);
85
91
 
86
- res.json({ success: true });
92
+ if (wasLive) {
93
+ await mcpClient.stopServer(server.id);
94
+ }
95
+
96
+ res.json({ success: true, status: 'stopped' });
87
97
  } catch (err) {
88
98
  res.status(400).json({ error: sanitizeError(err) });
89
99
  }
@@ -109,9 +119,8 @@ router.post('/:id/start', async (req, res) => {
109
119
 
110
120
  const mcpClient = req.app.locals.mcpClient;
111
121
  const result = await mcpClient.startServer(server.id, server.command, server.name, req.session.userId, { agentId: server.agent_id });
112
- const tools = await mcpClient.listTools(server.id, req.session.userId);
113
122
 
114
- res.json({ ...result, tools });
123
+ res.json(result);
115
124
  } catch (err) {
116
125
  if (err.message && err.message.startsWith('OAUTH_REDIRECT:')) {
117
126
  const url = err.message.substring(15);
@@ -152,20 +161,27 @@ router.get('/:id/tools', async (req, res) => {
152
161
  // OAuth Callback
153
162
  router.get('/oauth/callback', async (req, res) => {
154
163
  const { code, state, error } = req.query;
155
- if (!state) return res.status(400).send('Missing state parameter');
156
- if (!error && !code) return res.status(400).send('Missing code parameter');
157
- if (error) return res.status(400).send(`OAuth Error: ${error}`);
164
+ if (!state) return res.status(400).type('text/plain').send('Missing state parameter');
158
165
 
159
166
  const stateMatch = String(state).match(MCP_OAUTH_STATE_RE);
160
- if (!stateMatch) return res.status(400).send('Invalid state format');
167
+ if (!stateMatch) return res.status(400).type('text/plain').send('Invalid state format');
161
168
 
162
169
  const serverId = Number.parseInt(stateMatch[1], 10);
163
170
  if (!Number.isInteger(serverId) || serverId <= 0) {
164
- return res.status(400).send('Invalid state format');
171
+ return res.status(400).type('text/plain').send('Invalid state format');
165
172
  }
166
173
 
167
174
  const server = db.prepare('SELECT id FROM mcp_servers WHERE id = ? AND user_id = ?').get(serverId, req.session.userId);
168
- if (!server) return res.status(404).send('Server not found');
175
+ if (!server) return res.status(404).type('text/plain').send('Server not found');
176
+ if (!error && !code) {
177
+ return res.status(400).type('text/plain').send('Missing code parameter');
178
+ }
179
+ if (!consumeOAuthState(serverId, state)) {
180
+ return res.status(400).type('text/plain').send('Invalid or expired OAuth state');
181
+ }
182
+ if (error) {
183
+ return res.status(400).type('text/plain').send(`OAuth error: ${String(error)}`);
184
+ }
169
185
 
170
186
  const mcpClient = req.app.locals.mcpClient;
171
187
  const trustedOrigin = JSON.stringify(getTrustedPostMessageOrigin(req));
@@ -111,13 +111,19 @@ router.get('/', (req, res) => {
111
111
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
112
112
  const coreMemory = { ...(mm.getCoreMemory(userId, { agentId }) || {}) };
113
113
  delete coreMemory.active_context;
114
+ const knowledgeViews = mm.listKnowledgeViews(userId, { agentId, limit: 12 });
114
115
  res.json({
115
116
  agentId,
116
117
  assistantBehaviorNotes: mm.getAssistantBehaviorNotes(userId, { agentId }),
117
118
  assistantSelfState: mm.getAssistantSelfState(userId, { agentId }),
118
119
  dailyLogs: mm.listDailyLogs(7, userId),
119
120
  apiKeys: Object.keys(mm.readApiKeys(userId)),
120
- coreMemory
121
+ coreMemory,
122
+ stats: mm.getMemoryStats(userId, { agentId }),
123
+ entities: mm.listEntities(userId, { agentId, limit: 12 }),
124
+ knowledgeViews,
125
+ ingestionOverview: mm.getIngestionOverview(userId, { agentId }),
126
+ recentKnowledgeChanges: mm.listRecentKnowledgeChanges(userId, { agentId, limit: 8 }),
121
127
  });
122
128
  });
123
129
 
@@ -157,6 +163,7 @@ router.get('/ingestion/status', (req, res) => {
157
163
  jobs: mm.listIngestionJobs(userId, { agentId }),
158
164
  recentChanges: mm.listRecentKnowledgeChanges(userId, { agentId }),
159
165
  connections: ingestionService?.listConnectionStatuses?.(userId, { agentId }) || [],
166
+ service: ingestionService?.getStatus?.() || { state: 'unavailable' },
160
167
  });
161
168
  } catch (err) {
162
169
  res.status(500).json({ error: sanitizeError(err) });
@@ -204,6 +211,21 @@ router.get('/knowledge-views', (req, res) => {
204
211
  }
205
212
  });
206
213
 
214
+ router.get('/entities', (req, res) => {
215
+ const mm = req.app.locals.memoryManager;
216
+ const userId = req.session.userId;
217
+ const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
218
+ try {
219
+ res.json(mm.listEntities(userId, {
220
+ agentId,
221
+ query: req.query.query || null,
222
+ limit: req.query.limit,
223
+ }));
224
+ } catch (err) {
225
+ res.status(500).json({ error: sanitizeError(err) });
226
+ }
227
+ });
228
+
207
229
  router.post('/knowledge-views/materialize', (req, res) => {
208
230
  const mm = req.app.locals.memoryManager;
209
231
  const userId = req.session.userId;
@@ -80,6 +80,8 @@ router.post('/disconnect', async (req, res) => {
80
80
  try {
81
81
  const { platform } = req.body;
82
82
  const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
83
+ if (!platform) return res.status(400).json({ error: 'Platform is required' });
84
+
83
85
  const manager = req.app.locals.messagingManager;
84
86
  const result = await manager.disconnectPlatform(req.session.userId, platform, { agentId });
85
87
  res.json(result);
@@ -2,21 +2,24 @@
2
2
 
3
3
  const express = require('express');
4
4
  const db = require('../db/database');
5
+ const { buildFtsQuery } = require('../db/ftsQuery');
6
+ const { requireAuth } = require('../middleware/auth');
5
7
  const { getErrorMessage } = require('../services/bootstrap_helpers');
6
8
 
7
9
  const router = express.Router();
8
10
 
11
+ router.use(requireAuth);
12
+
9
13
  router.get('/search', (req, res) => {
10
14
  const { q, limit = 50, offset = 0 } = req.query;
11
-
12
- if (!req.user || !req.user.id) {
13
- return res.status(401).json({ error: 'Unauthorized' });
14
- }
15
+ const userId = req.session.userId;
15
16
 
16
17
  try {
17
18
  let results = [];
18
- if (q) {
19
- // Full text search
19
+ const ftsQuery = q ? buildFtsQuery(q) : null;
20
+ if (ftsQuery) {
21
+ // Full text search. buildFtsQuery sanitizes user input so FTS5 operator
22
+ // characters (hyphens, AND/OR/NOT) don't throw and 500 the request.
20
23
  results = db.prepare(`
21
24
  SELECT s.id, s.timestamp, s.app_name, s.text_content
22
25
  FROM screen_history_fts fts
@@ -24,7 +27,10 @@ router.get('/search', (req, res) => {
24
27
  WHERE screen_history_fts MATCH ? AND s.user_id = ?
25
28
  ORDER BY s.timestamp DESC
26
29
  LIMIT ? OFFSET ?
27
- `).all(q, req.user.id, Number(limit), Number(offset));
30
+ `).all(ftsQuery, userId, Number(limit), Number(offset));
31
+ } else if (q) {
32
+ // Query had no usable search tokens — return no matches rather than error.
33
+ results = [];
28
34
  } else {
29
35
  // Recent history
30
36
  results = db.prepare(`
@@ -33,7 +39,7 @@ router.get('/search', (req, res) => {
33
39
  WHERE user_id = ?
34
40
  ORDER BY timestamp DESC
35
41
  LIMIT ? OFFSET ?
36
- `).all(req.user.id, Number(limit), Number(offset));
42
+ `).all(userId, Number(limit), Number(offset));
37
43
  }
38
44
 
39
45
  res.json({ results });
@@ -297,7 +297,11 @@ router.put('/', async (req, res) => {
297
297
  'runtime_profile' in normalizedBody
298
298
  || 'runtime_backend' in normalizedBody
299
299
  || 'browser_backend' in normalizedBody
300
+ || 'browser_extension_token_id' in normalizedBody
301
+ || 'selected_browser_extension_token_id' in normalizedBody
300
302
  || 'android_backend' in normalizedBody
303
+ || 'cli_backend' in normalizedBody
304
+ || 'cli_desktop_device_id' in normalizedBody
301
305
  || 'mcp_backend' in normalizedBody
302
306
  ) {
303
307
  const validation = validateRuntimeSettings({
@@ -558,8 +562,16 @@ router.delete('/:key', (req, res) => {
558
562
  res.json({ success: true });
559
563
  });
560
564
 
565
+ function requireAdminSession(req, res, next) {
566
+ if (req.session?.isAdmin === true) return next();
567
+ return res.status(403).json({
568
+ success: false,
569
+ error: 'Server updates are only available from the admin dashboard.',
570
+ });
571
+ }
572
+
561
573
  // Trigger auto-update script
562
- router.post('/update', requireAuth, updateTriggerLimiter, (req, res) => {
574
+ router.post('/update', requireAuth, requireAdminSession, updateTriggerLimiter, (req, res) => {
563
575
  if (isManagedDeployment()) {
564
576
  return res.status(403).json({
565
577
  success: false,
@@ -607,7 +619,7 @@ router.post('/update', requireAuth, updateTriggerLimiter, (req, res) => {
607
619
  res.json({ success: true, message: 'Update triggered', pid: child.pid });
608
620
  });
609
621
 
610
- router.put('/update/channel', (req, res) => {
622
+ router.put('/update/channel', requireAuth, requireAdminSession, (req, res) => {
611
623
  if (isManagedDeployment()) {
612
624
  return res.status(403).json({
613
625
  success: false,
@@ -111,7 +111,7 @@ router.post('/start', async (req, res) => {
111
111
  const platform = normalizePlatform(req.body?.platform);
112
112
  const resolved = await resolveStartDeviceId(platform, req);
113
113
  const deviceId = resolved.deviceId;
114
- const fps = boundedInt(req.body?.fps, platform === 'android' ? 10 : 15, 1, platform === 'android' ? 15 : 20);
114
+ const fps = boundedInt(req.body?.fps, platform === 'android' ? 12 : 15, 1, platform === 'android' ? 30 : 20);
115
115
  const quality = boundedInt(req.body?.quality, platform === 'android' ? 75 : 80, 30, 95);
116
116
  const hub = streamHub(req);
117
117
  await hub.stopStream(userId, platform, deviceId, 'restart');
@@ -125,8 +125,17 @@ router.post('/start', async (req, res) => {
125
125
  displayId: req.body?.displayId || null,
126
126
  });
127
127
  const resolvedDeviceId = result?.deviceId || result?.device?.deviceId || deviceId;
128
- hub.markStarted(userId, resolvedDeviceId, platform, { fps, quality }, () =>
129
- provider.stopStream({ deviceId: resolvedDeviceId }));
128
+ hub.markStarted(userId, resolvedDeviceId, platform, { fps, quality }, async () => {
129
+ try {
130
+ await provider.stopStream({ deviceId: resolvedDeviceId });
131
+ } catch (error) {
132
+ console.error('[StreamRoute] failed to stop desktop stream', {
133
+ userId,
134
+ deviceId: resolvedDeviceId,
135
+ error: String(error?.message || error),
136
+ });
137
+ }
138
+ });
130
139
  return res.json({ ok: true, platform, deviceId: resolvedDeviceId, fps, quality });
131
140
  }
132
141
 
@@ -13,10 +13,10 @@ router.post('/geofence', async (req, res) => {
13
13
  const { label, latitude, longitude, radius_meters, action } = req.body;
14
14
 
15
15
  try {
16
- const userRow = db.prepare('SELECT id FROM users WHERE id = ?').get(req.user.id);
16
+ const userRow = db.prepare('SELECT id FROM users WHERE id = ?').get(req.session.userId);
17
17
  if (!userRow) return res.status(401).json({ error: 'Unauthorized' });
18
18
 
19
- console.log(`[Triggers] Geofence entered: ${label} by user ${req.user.id}`);
19
+ console.log(`[Triggers] Geofence entered: ${label} by user ${req.session.userId}`);
20
20
 
21
21
  res.json({ success: true, message: 'Geofence trigger processed' });
22
22
  } catch (err) {
@@ -29,14 +29,14 @@ router.post('/geofence', async (req, res) => {
29
29
  Promise.resolve().then(() => {
30
30
  const agentEngine = req.app.locals.agentEngine;
31
31
  if (!agentEngine) return;
32
- const defaultAgentId = db.prepare('SELECT id FROM agents WHERE user_id = ? ORDER BY is_default DESC LIMIT 1').get(req.user.id)?.id;
32
+ const defaultAgentId = db.prepare('SELECT id FROM agents WHERE user_id = ? ORDER BY is_default DESC LIMIT 1').get(req.session.userId)?.id;
33
33
  if (!defaultAgentId) return;
34
34
  const prompt = [
35
35
  `A geofence event was triggered.`,
36
36
  `Location label: ${label || 'unknown'}`,
37
37
  action ? `Suggested action: ${action}` : 'Check if there are any active reminders or tasks related to this location.',
38
38
  ].join('\n');
39
- return agentEngine.run(req.user.id, prompt, {
39
+ return agentEngine.run(req.session.userId, prompt, {
40
40
  agentId: defaultAgentId,
41
41
  triggerSource: 'tasks',
42
42
  context: { source: 'geofence', label, latitude, longitude, radius_meters },
@@ -4,7 +4,8 @@ const { requireAuth } = require('../middleware/auth');
4
4
  const { sanitizeError } = require('../utils/security');
5
5
  const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
6
6
  const { TurnCoordinator } = require('../services/voice/turnCoordinator');
7
- const { normalizeVoiceSynthesisOptions } = require('../services/voice/providers');
7
+ const { normalizeVoiceSynthesisOptions, transcribeVoiceInput } = require('../services/voice/providers');
8
+ const { writeTempAudioFile, removeTempFile } = require('../services/voice/liveAudio');
8
9
  const { runVoiceTranscriptTurn } = require('../services/voice/turnRunner');
9
10
 
10
11
  const router = express.Router();
@@ -135,4 +136,38 @@ router.post('/respond', async (req, res) => {
135
136
  }
136
137
  });
137
138
 
139
+ router.post('/transcribe', async (req, res) => {
140
+ try {
141
+ const audioBase64 = String(req.body?.audioBase64 || '').trim();
142
+ const mimeType = String(req.body?.mimeType || 'audio/pcm;rate=16000;channels=1').trim();
143
+
144
+ if (!audioBase64) {
145
+ return res.status(400).json({ error: 'audioBase64 is required.' });
146
+ }
147
+
148
+ const approxBytes = (audioBase64.length * 3) / 4;
149
+ if (approxBytes > 25 * 1024 * 1024) {
150
+ return res.status(400).json({ error: 'Audio exceeds maximum size of 25MB.' });
151
+ }
152
+
153
+ const audioBytes = Buffer.from(audioBase64, 'base64');
154
+ const { filePath, mimeType: fileMimeType } = await writeTempAudioFile(audioBytes, mimeType);
155
+ let transcript = '';
156
+ try {
157
+ transcript = await transcribeVoiceInput(filePath, {
158
+ mimeType: fileMimeType,
159
+ userId: req.session.userId,
160
+ timeoutMs: 30000,
161
+ });
162
+ } finally {
163
+ await removeTempFile(filePath);
164
+ }
165
+
166
+ return res.json({ transcript: String(transcript || '').trim() });
167
+ } catch (err) {
168
+ const message = sanitizeError(err);
169
+ return res.status(500).json({ error: message });
170
+ }
171
+ });
172
+
138
173
  module.exports = router;
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const { requireAuth } = require('../middleware/auth');
5
+ const { sanitizeError } = require('../utils/security');
6
+
7
+ const router = express.Router();
8
+
9
+ const MAX_PATH_LENGTH = 2048;
10
+ const MAX_EDIT_BYTES = 1024 * 1024;
11
+
12
+ router.use(requireAuth);
13
+
14
+ function badRequest(message) {
15
+ const error = new Error(message);
16
+ error.status = 400;
17
+ return error;
18
+ }
19
+
20
+ function parseWorkspacePath(value, fallback = '.') {
21
+ const normalized = String(value ?? fallback).trim();
22
+ if (normalized.length > MAX_PATH_LENGTH) {
23
+ throw badRequest('path is too long.');
24
+ }
25
+ return normalized || fallback;
26
+ }
27
+
28
+ function getWorkspace(req) {
29
+ const workspace = req.app?.locals?.workspaceManager;
30
+ if (!workspace) {
31
+ const error = new Error('Workspace service is unavailable.');
32
+ error.status = 503;
33
+ throw error;
34
+ }
35
+ return workspace;
36
+ }
37
+
38
+ function handleWorkspaceAction(req, res, action) {
39
+ try {
40
+ const result = action(getWorkspace(req));
41
+ return res.json(result);
42
+ } catch (err) {
43
+ return res.status(err.status || 500).json({ error: sanitizeError(err), code: err.code || null });
44
+ }
45
+ }
46
+
47
+ router.get('/files', (req, res) => handleWorkspaceAction(req, res, (workspace) =>
48
+ workspace.listExplorerDirectory(req.session.userId, {
49
+ path: parseWorkspacePath(req.query?.path, '.'),
50
+ })));
51
+
52
+ router.get('/files/content', (req, res) => handleWorkspaceAction(req, res, (workspace) =>
53
+ workspace.readExplorerFile(req.session.userId, {
54
+ path: parseWorkspacePath(req.query?.path, ''),
55
+ maxBytes: MAX_EDIT_BYTES,
56
+ })));
57
+
58
+ router.put('/files/content', (req, res) => handleWorkspaceAction(req, res, (workspace) => {
59
+ const content = String(req.body?.content ?? '');
60
+ if (Buffer.byteLength(content, 'utf8') > MAX_EDIT_BYTES) {
61
+ throw badRequest('content is too large.');
62
+ }
63
+ return workspace.writeExplorerFile(req.session.userId, {
64
+ path: parseWorkspacePath(req.body?.path, ''),
65
+ content,
66
+ });
67
+ }));
68
+
69
+ router.get('/files/download', (req, res) => {
70
+ try {
71
+ const workspace = getWorkspace(req);
72
+ const file = workspace.getExplorerDownload(req.session.userId, {
73
+ path: parseWorkspacePath(req.query?.path, ''),
74
+ });
75
+ res.setHeader('Cache-Control', 'private, no-store');
76
+ return res.download(file.absolutePath, file.filename, (err) => {
77
+ if (!err || res.headersSent) return;
78
+ const status = err.code === 'ENOENT' || err.code === 'EACCES' ? 404 : 500;
79
+ res.status(status).json({ error: status === 404 ? 'File not found' : 'Failed to download file' });
80
+ });
81
+ } catch (err) {
82
+ return res.status(err.status || 500).json({ error: sanitizeError(err), code: err.code || null });
83
+ }
84
+ });
85
+
86
+ module.exports = router;
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ const bcrypt = require('bcrypt');
4
+ const crypto = require('crypto');
5
+ const { generateSecret, generateURI, verifySync } = require('otplib');
6
+ const db = require('../../db/database');
7
+ const { encryptValue, decryptValue } = require('../integrations/secrets');
8
+
9
+ const TOTP_OPTS = { strategy: 'totp', digits: 6, period: 30, epochTolerance: 30 };
10
+ const RECOVERY_ALPHA = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
11
+
12
+ // ── Helpers ────────────────────────────────────────────────────────────────
13
+
14
+ function getRow() {
15
+ return db.prepare('SELECT * FROM admin_two_factor WHERE id = 1').get() || null;
16
+ }
17
+
18
+ function normalizeCode(v) {
19
+ return String(v || '').trim().replace(/\s+/g, '').toUpperCase();
20
+ }
21
+
22
+ function verifyTotpCode(secret, code) {
23
+ const token = String(code || '').trim().replace(/\s+/g, '');
24
+ if (!/^\d{6}$/.test(token)) return false;
25
+ return verifySync({ ...TOTP_OPTS, secret, token })?.valid === true;
26
+ }
27
+
28
+ function makeRecoveryCode() {
29
+ let raw = '';
30
+ while (raw.length < 10) raw += RECOVERY_ALPHA[crypto.randomInt(RECOVERY_ALPHA.length)];
31
+ return `${raw.slice(0, 5)}-${raw.slice(5)}`;
32
+ }
33
+
34
+ // ── Public API ─────────────────────────────────────────────────────────────
35
+
36
+ function getStatus() {
37
+ const row = getRow();
38
+ const codesLeft = db.prepare('SELECT COUNT(*) AS n FROM admin_recovery_codes WHERE used_at IS NULL').get().n;
39
+ return {
40
+ enabled: row?.enabled === 1,
41
+ pending: Boolean(row?.pending_secret),
42
+ enabledAt: row?.enabled_at || null,
43
+ recoveryCodesRemaining: codesLeft,
44
+ };
45
+ }
46
+
47
+ function beginSetup() {
48
+ if (!String(process.env.SESSION_SECRET || '').trim()) {
49
+ throw Object.assign(new Error('SESSION_SECRET must be configured before enabling 2FA'), { statusCode: 500 });
50
+ }
51
+ const secret = generateSecret();
52
+ const otpauthUrl = generateURI({ ...TOTP_OPTS, issuer: 'NeoAgent Admin', label: 'admin', secret });
53
+ db.prepare(`
54
+ INSERT INTO admin_two_factor (id, pending_secret, enabled, created_at, updated_at)
55
+ VALUES (1, ?, COALESCE((SELECT enabled FROM admin_two_factor WHERE id = 1), 0), datetime('now'), datetime('now'))
56
+ ON CONFLICT(id) DO UPDATE SET pending_secret = excluded.pending_secret, updated_at = datetime('now')
57
+ `).run(encryptValue(secret));
58
+ return { otpauthUrl, manualKey: secret };
59
+ }
60
+
61
+ async function enable(code) {
62
+ const row = getRow();
63
+ if (!row?.pending_secret) throw Object.assign(new Error('No 2FA setup pending'), { statusCode: 400 });
64
+ const secret = decryptValue(row.pending_secret);
65
+ if (!verifyTotpCode(secret, code)) throw Object.assign(new Error('Invalid code — try again'), { statusCode: 401 });
66
+
67
+ const codes = Array.from({ length: 10 }, makeRecoveryCode);
68
+ const hashes = await Promise.all(codes.map((c) => bcrypt.hash(normalizeCode(c).replace(/-/g, ''), 12)));
69
+
70
+ db.transaction(() => {
71
+ db.prepare('DELETE FROM admin_recovery_codes').run();
72
+ const ins = db.prepare("INSERT INTO admin_recovery_codes (code_hash, created_at) VALUES (?, datetime('now'))");
73
+ for (const h of hashes) ins.run(h);
74
+ db.prepare(`
75
+ UPDATE admin_two_factor
76
+ SET secret = pending_secret, pending_secret = NULL, enabled = 1,
77
+ enabled_at = datetime('now'), updated_at = datetime('now')
78
+ WHERE id = 1
79
+ `).run();
80
+ })();
81
+
82
+ return { recoveryCodes: codes };
83
+ }
84
+
85
+ /**
86
+ * Verifies a TOTP code or recovery code.
87
+ * Returns true if valid (or if 2FA is not enabled).
88
+ */
89
+ async function verifyCode(code) {
90
+ const row = getRow();
91
+ if (!row?.enabled || !row.secret) return true; // 2FA not configured
92
+
93
+ const secret = decryptValue(row.secret);
94
+ if (verifyTotpCode(secret, code)) return true;
95
+
96
+ // Try recovery code
97
+ const normalized = normalizeCode(String(code || '')).replace(/-/g, '');
98
+ if (normalized.length < 6) return false;
99
+ const rows = db.prepare('SELECT id, code_hash FROM admin_recovery_codes WHERE used_at IS NULL ORDER BY id').all();
100
+ for (const r of rows) {
101
+ if (await bcrypt.compare(normalized, r.code_hash)) {
102
+ db.prepare("UPDATE admin_recovery_codes SET used_at = datetime('now') WHERE id = ?").run(r.id);
103
+ return true;
104
+ }
105
+ }
106
+ return false;
107
+ }
108
+
109
+ async function disable(code) {
110
+ const row = getRow();
111
+ if (!row?.enabled) return;
112
+ if (!await verifyCode(code)) throw Object.assign(new Error('Invalid 2FA code'), { statusCode: 401 });
113
+ db.transaction(() => {
114
+ db.prepare(`UPDATE admin_two_factor SET enabled = 0, secret = NULL, pending_secret = NULL,
115
+ enabled_at = NULL, updated_at = datetime('now') WHERE id = 1`).run();
116
+ db.prepare('DELETE FROM admin_recovery_codes').run();
117
+ })();
118
+ }
119
+
120
+ async function regenerateCodes(code) {
121
+ if (!await verifyCode(code)) throw Object.assign(new Error('Invalid 2FA code'), { statusCode: 401 });
122
+ const codes = Array.from({ length: 10 }, makeRecoveryCode);
123
+ const hashes = await Promise.all(codes.map((c) => bcrypt.hash(normalizeCode(c).replace(/-/g, ''), 12)));
124
+ db.transaction(() => {
125
+ db.prepare('DELETE FROM admin_recovery_codes').run();
126
+ const ins = db.prepare("INSERT INTO admin_recovery_codes (code_hash, created_at) VALUES (?, datetime('now'))");
127
+ for (const h of hashes) ins.run(h);
128
+ })();
129
+ return { recoveryCodes: codes };
130
+ }
131
+
132
+ module.exports = { getStatus, beginSetup, enable, verifyCode, disable, regenerateCodes };
@@ -113,7 +113,12 @@ function evaluatePasswordStrength(password, context = {}) {
113
113
  label,
114
114
  length,
115
115
  hasMinimumLength: length >= MIN_PASSWORD_LENGTH,
116
- isAcceptable: length >= MIN_PASSWORD_LENGTH && score >= MIN_PASSWORD_SCORE,
116
+ isAcceptable: length >= MIN_PASSWORD_LENGTH
117
+ && score >= MIN_PASSWORD_SCORE
118
+ && !containsPersonalInfo
119
+ && !usesCommonPattern
120
+ && !sequential
121
+ && !repeatedRuns,
117
122
  feedback,
118
123
  };
119
124
  }