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.
- package/frontend/dist/assets/index-CGqvdwHT.js +5742 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/config.ts +16 -2
- package/frontend/src/api/keys.ts +111 -0
- package/frontend/src/api/notifications.ts +54 -6
- package/frontend/src/api/update.ts +12 -1
- package/frontend/src/components/BrainSelector.spec.tsx +71 -47
- package/frontend/src/components/BrainSelector.tsx +112 -35
- package/frontend/src/components/MobilePairing.spec.tsx +44 -14
- package/frontend/src/components/MobilePairing.tsx +229 -22
- package/frontend/src/pages/NotificationsPage.tsx +6 -3
- package/frontend/src/pages/RulesPage.spec.tsx +46 -15
- package/frontend/src/pages/RulesPage.tsx +41 -9
- package/frontend/src/pages/SecretsPage.tsx +316 -3
- package/frontend/src/pages/SettingsPage.spec.tsx +30 -6
- package/frontend/src/pages/SettingsPage.tsx +206 -85
- package/package.json +1 -1
- package/src/cli/agent-dir.mjs +2 -1
- package/src/cli/secret.mjs +180 -3
- package/src/core/daemon-loop.mjs +31 -5
- package/src/core/notifications.mjs +204 -34
- package/src/core/package-auto-update.mjs +99 -3
- package/src/core/pairing.mjs +168 -0
- package/src/core/provider-account-sync.mjs +1145 -0
- package/src/core/provider-catalog.mjs +11 -4
- package/src/core/research.mjs +98 -24
- package/src/core/scheduler.mjs +31 -9
- package/src/core/schema.mjs +23 -0
- package/src/core/secret-integration-research.mjs +325 -0
- package/src/core/secrets-store.mjs +278 -14
- package/src/core/secrets-sync.mjs +1 -1
- package/src/core/source-adapters.mjs +247 -22
- package/src/core/task-executors.mjs +161 -14
- package/src/core/webhook-handlers.mjs +123 -14
- package/src/server/auth.mjs +6 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +24 -0
- package/src/server/routes/config.mjs +64 -23
- package/src/server/routes/mesh.mjs +2 -1
- package/src/server/routes/notifications.mjs +103 -0
- package/src/server/routes/rules.mjs +75 -20
- package/src/server/routes/secrets.mjs +110 -4
- package/src/server/routes/system.mjs +20 -1
- package/src/server/routes/update.mjs +11 -4
- 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(),
|
|
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
|
-
},
|
|
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(),
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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,
|