total-recall-brain 3.18.2 → 3.18.3

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 (45) hide show
  1. package/frontend/dist/assets/index-CGqvdwHT.js +5742 -0
  2. package/frontend/dist/index.html +1 -1
  3. package/frontend/src/api/config.ts +16 -2
  4. package/frontend/src/api/keys.ts +111 -0
  5. package/frontend/src/api/notifications.ts +54 -6
  6. package/frontend/src/api/update.ts +12 -1
  7. package/frontend/src/components/BrainSelector.spec.tsx +71 -47
  8. package/frontend/src/components/BrainSelector.tsx +112 -35
  9. package/frontend/src/components/MobilePairing.spec.tsx +44 -14
  10. package/frontend/src/components/MobilePairing.tsx +229 -22
  11. package/frontend/src/pages/NotificationsPage.tsx +6 -3
  12. package/frontend/src/pages/RulesPage.spec.tsx +46 -15
  13. package/frontend/src/pages/RulesPage.tsx +41 -9
  14. package/frontend/src/pages/SecretsPage.tsx +316 -3
  15. package/frontend/src/pages/SettingsPage.spec.tsx +30 -6
  16. package/frontend/src/pages/SettingsPage.tsx +206 -85
  17. package/package.json +1 -1
  18. package/src/cli/agent-dir.mjs +2 -1
  19. package/src/cli/secret.mjs +180 -3
  20. package/src/core/daemon-loop.mjs +31 -5
  21. package/src/core/notifications.mjs +204 -34
  22. package/src/core/package-auto-update.mjs +99 -3
  23. package/src/core/pairing.mjs +168 -0
  24. package/src/core/provider-account-sync.mjs +1145 -0
  25. package/src/core/provider-catalog.mjs +11 -4
  26. package/src/core/research.mjs +98 -24
  27. package/src/core/scheduler.mjs +31 -9
  28. package/src/core/schema.mjs +23 -0
  29. package/src/core/secret-integration-research.mjs +325 -0
  30. package/src/core/secrets-store.mjs +278 -14
  31. package/src/core/secrets-sync.mjs +1 -1
  32. package/src/core/source-adapters.mjs +247 -22
  33. package/src/core/task-executors.mjs +161 -14
  34. package/src/core/webhook-handlers.mjs +123 -14
  35. package/src/server/auth.mjs +6 -0
  36. package/src/server/rest.mjs +2 -0
  37. package/src/server/route-manifest.json +24 -0
  38. package/src/server/routes/config.mjs +64 -23
  39. package/src/server/routes/mesh.mjs +2 -1
  40. package/src/server/routes/notifications.mjs +103 -0
  41. package/src/server/routes/rules.mjs +75 -20
  42. package/src/server/routes/secrets.mjs +110 -4
  43. package/src/server/routes/system.mjs +20 -1
  44. package/src/server/routes/update.mjs +11 -4
  45. package/frontend/dist/assets/index-D5xRw5uq.js +0 -5750
@@ -118,15 +118,18 @@ router.get('/api/secrets/sync/status', requireAuth, requireScope('keys:read', 'c
118
118
  const peers = getMeshPeers();
119
119
  const authorization = await getMeshSyncAuthorization();
120
120
 
121
+ // Mesh peers over Tailscale can take multi-second RTT under load (WAN laptop↔cloud).
122
+ // 1.5s was too aggressive and reported healthy peers as unreachable.
123
+ const PEER_PROBE_TIMEOUT_MS = 10_000;
121
124
  const nodes = await Promise.all(peers.map(async (peer) => {
122
125
  try {
123
126
  const controller = new AbortController();
124
- const timeoutId = setTimeout(() => controller.abort(), 1500);
127
+ const timeoutId = setTimeout(() => controller.abort(), PEER_PROBE_TIMEOUT_MS);
125
128
 
126
129
  const response = await throttledFetch(meshPeerUrl(peer.ip, '/api/secrets/checksum'), {
127
130
  signal: controller.signal,
128
131
  headers: { Authorization: authorization }
129
- }, 1_500);
132
+ }, PEER_PROBE_TIMEOUT_MS);
130
133
  clearTimeout(timeoutId);
131
134
 
132
135
  if (response.ok) {
@@ -164,10 +167,11 @@ router.post('/api/secrets/sync/trigger', requireAuth, requireScope('keys:write',
164
167
  const peers = getMeshPeers();
165
168
  const authorization = await getMeshSyncAuthorization();
166
169
 
170
+ const PEER_TRIGGER_TIMEOUT_MS = 10_000;
167
171
  const results = await Promise.all(peers.map(async (peer) => {
168
172
  try {
169
173
  const controller = new AbortController();
170
- const timeoutId = setTimeout(() => controller.abort(), 1500);
174
+ const timeoutId = setTimeout(() => controller.abort(), PEER_TRIGGER_TIMEOUT_MS);
171
175
 
172
176
  const response = await throttledFetch(meshPeerUrl(peer.ip, '/api/secrets/sync/trigger-pull'), {
173
177
  method: 'POST',
@@ -176,7 +180,7 @@ router.post('/api/secrets/sync/trigger', requireAuth, requireScope('keys:write',
176
180
  'Content-Type': 'application/json',
177
181
  Authorization: authorization
178
182
  }
179
- }, 1_500);
183
+ }, PEER_TRIGGER_TIMEOUT_MS);
180
184
  clearTimeout(timeoutId);
181
185
 
182
186
  return {
@@ -233,6 +237,108 @@ router.get('/api/secrets/providers', requireAuth, requireScope('keys:read', 'con
233
237
  res.json({ providers: listProviders() });
234
238
  });
235
239
 
240
+ /**
241
+ * GET /api/secrets/tracking-health
242
+ * Hard status: any set secret without tracking_status=ok|exempt is an error.
243
+ */
244
+ router.get(
245
+ '/api/secrets/tracking-health',
246
+ requireAuth,
247
+ requireScope('keys:read', 'config:read'),
248
+ async (req, res) => {
249
+ try {
250
+ const brainDir = brainDirFromReq(req);
251
+ const { getTrackingHealth } = await import('../../core/provider-account-sync.mjs');
252
+ const health = await getTrackingHealth(brainDir);
253
+ res.status(health.healthy ? 200 : 409).json(health);
254
+ } catch (err) {
255
+ serverError(res, err);
256
+ }
257
+ },
258
+ );
259
+
260
+ /**
261
+ * GET /api/secrets/shared-values
262
+ * Same credential material under multiple secret names / apps → ERROR (rotate per app).
263
+ */
264
+ router.get(
265
+ '/api/secrets/shared-values',
266
+ requireAuth,
267
+ requireScope('keys:read', 'config:read'),
268
+ async (req, res) => {
269
+ try {
270
+ const brainDir = brainDirFromReq(req);
271
+ const { getSharedValueHealth } = await import('../../core/secrets-store.mjs');
272
+ const health = await getSharedValueHealth(brainDir);
273
+ res.status(health.healthy ? 200 : 409).json(health);
274
+ } catch (err) {
275
+ serverError(res, err);
276
+ }
277
+ },
278
+ );
279
+
280
+ /**
281
+ * POST /api/secrets/account-sync
282
+ * Live-probe vendor account/usage/subscription APIs and persist tracking meta.
283
+ * Body: { key?, keys?, strict?, use_ai? }
284
+ */
285
+ router.post(
286
+ '/api/secrets/account-sync',
287
+ requireAuth,
288
+ requireScope('keys:write', 'config:write'),
289
+ async (req, res) => {
290
+ try {
291
+ const brainDir = brainDirFromReq(req);
292
+ const body = req.body || {};
293
+ const {
294
+ syncSecretAccount,
295
+ syncAllSecretAccounts,
296
+ } = await import('../../core/provider-account-sync.mjs');
297
+ const opts = {
298
+ strict: body.strict !== false,
299
+ use_ai: !!body.use_ai,
300
+ force_exempt: !!body.force_exempt,
301
+ };
302
+ if (body.key) {
303
+ const result = await syncSecretAccount(brainDir, body.key, opts);
304
+ const ok = result.tracking_status === 'ok' || result.tracking_status === 'exempt';
305
+ return res.status(ok ? 200 : 409).json(result);
306
+ }
307
+ if (Array.isArray(body.keys) && body.keys.length) {
308
+ opts.keys = body.keys;
309
+ }
310
+ const report = await syncAllSecretAccounts(brainDir, opts);
311
+ res.status(report.healthy ? 200 : 409).json(report);
312
+ } catch (err) {
313
+ serverError(res, err);
314
+ }
315
+ },
316
+ );
317
+
318
+ /**
319
+ * POST /api/secrets/:key/account-sync — sync one secret by path param
320
+ */
321
+ router.post(
322
+ '/api/secrets/:key/account-sync',
323
+ requireAuth,
324
+ requireScope('keys:write', 'config:write'),
325
+ async (req, res) => {
326
+ try {
327
+ const brainDir = brainDirFromReq(req);
328
+ const { syncSecretAccount } = await import('../../core/provider-account-sync.mjs');
329
+ const result = await syncSecretAccount(brainDir, req.params.key, {
330
+ strict: req.body?.strict !== false,
331
+ use_ai: !!req.body?.use_ai,
332
+ force_exempt: !!req.body?.force_exempt,
333
+ });
334
+ const ok = result.tracking_status === 'ok' || result.tracking_status === 'exempt';
335
+ res.status(ok ? 200 : 409).json(result);
336
+ } catch (err) {
337
+ serverError(res, err);
338
+ }
339
+ },
340
+ );
341
+
236
342
  router.get('/api/secrets/usage', requireAuth, requireScope('keys:read', 'config:read'), async (req, res) => {
237
343
  try {
238
344
  const brainDir = brainDirFromReq(req);
@@ -115,7 +115,26 @@ router.post('/api/diagnostics/agents', requireAuth, requireScope('health:read'),
115
115
  }
116
116
  });
117
117
 
118
-
118
+ /**
119
+ * GET /api/pairing
120
+ * Reachable URLs for the Settings mobile pairing QR (LAN / mesh / loopback).
121
+ */
122
+ router.get('/api/pairing', requireAuth, requireScope('config:read'), async (_req, res) => {
123
+ try {
124
+ const { buildPairingInfo } = await import('../../core/pairing.mjs');
125
+ const { getMeshIp } = await import('../../core/mesh.mjs');
126
+ const protocol = _req.secure || String(_req.headers['x-forwarded-proto'] || '').includes('https')
127
+ ? 'https'
128
+ : 'http';
129
+ const info = buildPairingInfo({
130
+ protocol,
131
+ meshIp: getMeshIp(),
132
+ });
133
+ res.json(info);
134
+ } catch (err) {
135
+ serverError(res, err);
136
+ }
137
+ });
119
138
 
120
139
  /**
121
140
  * GET /api/tasks/failed
@@ -6,7 +6,7 @@ import { ROOT, serverError } from './_shared.mjs';
6
6
  import { brainDir as configBrainDir } from '../../core/config.mjs';
7
7
  import {
8
8
  PACKAGE_NAME,
9
- fetchLatestNpmVersion,
9
+ fetchLatestNpmVersionAsync,
10
10
  inspectProjectPackage,
11
11
  listUpdateRoots,
12
12
  runPackageAutoUpdate,
@@ -27,7 +27,8 @@ router.get('/api/update/check', requireAuth, async (_req, res) => {
27
27
  current = null;
28
28
  }
29
29
 
30
- const latest = fetchLatestNpmVersion() || '';
30
+ // Non-blocking npm view (spawnSync blocked the whole server and froze Settings).
31
+ const latest = (await fetchLatestNpmVersionAsync({ timeoutMs: 8_000 })) || '';
31
32
  const roots = listUpdateRoots({ brainDir: configBrainDir });
32
33
  const projects = roots.map(({ root, name, source }) => {
33
34
  const info = inspectProjectPackage(root);
@@ -43,12 +44,18 @@ router.get('/api/update/check', requireAuth, async (_req, res) => {
43
44
  });
44
45
 
45
46
  const consumersBehind = projects.filter((p) => p.update_available).length;
47
+ const updateAvailable =
48
+ Boolean(latest && current && current !== latest) || consumersBehind > 0;
46
49
 
47
50
  res.json({
48
51
  package: PACKAGE_NAME,
49
52
  current,
53
+ currentVersion: current,
50
54
  latest,
51
- update_available: Boolean(latest && current && current !== latest) || consumersBehind > 0,
55
+ latestVersion: latest,
56
+ // snake + camel for dashboard clients
57
+ update_available: updateAvailable,
58
+ updateAvailable,
52
59
  auto_update_enabled: isPackageAutoUpdateEnabled(),
53
60
  projects,
54
61
  consumers_behind: consumersBehind,
@@ -82,7 +89,7 @@ router.post('/api/update/run', requireAuth, requireScope('config:write'), async
82
89
  // Also kick a local install in ROOT when this host is a consumer (non-source)
83
90
  const hostInfo = inspectProjectPackage(ROOT);
84
91
  if (!hostInfo.isSourceTree && (hostInfo.declared || hostInfo.installed) && !dryRun) {
85
- const latest = summary.latest || fetchLatestNpmVersion();
92
+ const latest = summary.latest || (await fetchLatestNpmVersionAsync());
86
93
  if (latest && needsUpdate(hostInfo.installed, latest)) {
87
94
  const proc = spawn('npm', ['install', `${PACKAGE_NAME}@${latest}`, '--save'], {
88
95
  cwd: ROOT,