total-recall-brain 3.12.2 → 3.13.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 (154) hide show
  1. package/README.md +91 -108
  2. package/bin/antigravity.mjs +81 -52
  3. package/bin/total-recall.mjs +32 -57
  4. package/extension/background.js +1 -1
  5. package/extension/options/options.js +9 -3
  6. package/extension/sidepanel/sidepanel.js +0 -2
  7. package/frontend/index.html +4 -3
  8. package/frontend/package.json +3 -1
  9. package/frontend/src/App.tsx +112 -111
  10. package/frontend/src/api.ts +700 -18
  11. package/frontend/src/components/BrainSelector.tsx +211 -199
  12. package/frontend/src/components/DocumentEditorModal.tsx +57 -0
  13. package/frontend/src/components/DocumentTable.tsx +47 -0
  14. package/frontend/src/components/Graph3D.tsx +120 -71
  15. package/frontend/src/components/UsageChart.tsx +78 -0
  16. package/frontend/src/components/brand/BrandMark.tsx +95 -0
  17. package/frontend/src/index.css +286 -86
  18. package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
  19. package/frontend/src/pages/ChatPage.tsx +103 -45
  20. package/frontend/src/pages/DesignDocsPage.tsx +614 -0
  21. package/frontend/src/pages/FilesPage.tsx +10 -6
  22. package/frontend/src/pages/GraphPage.tsx +5 -3
  23. package/frontend/src/pages/HelpPage.tsx +2 -2
  24. package/frontend/src/pages/InboxPage.tsx +82 -0
  25. package/frontend/src/pages/InstructionsPage.tsx +583 -0
  26. package/frontend/src/pages/IntegrationsPage.tsx +21 -10
  27. package/frontend/src/pages/LoginPage.tsx +95 -77
  28. package/frontend/src/pages/MemoryPage.tsx +31 -8
  29. package/frontend/src/pages/ModelsPage.tsx +280 -23
  30. package/frontend/src/pages/OkfPage.tsx +535 -0
  31. package/frontend/src/pages/OnboardingPage.tsx +690 -0
  32. package/frontend/src/pages/OpenWikiPage.tsx +561 -0
  33. package/frontend/src/pages/SettingsPage.tsx +826 -740
  34. package/frontend/src/pages/SkillsPage.tsx +1259 -117
  35. package/frontend/src/pages/UsagePage.tsx +256 -11
  36. package/frontend/src/pages/VaultPage.tsx +223 -0
  37. package/frontend/src/types.ts +173 -0
  38. package/package.json +4 -4
  39. package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
  40. package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
  41. package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
  42. package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
  43. package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
  44. package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
  45. package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
  46. package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
  47. package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
  48. package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
  49. package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
  50. package/src/cli/brain.mjs +63 -5
  51. package/src/cli/connect.mjs +11 -19
  52. package/src/cli/daemon.mjs +5 -4
  53. package/src/cli/deploy-ui.mjs +8 -3
  54. package/src/cli/deploy.mjs +84 -27
  55. package/src/cli/ingest/index.mjs +1 -1
  56. package/src/cli/init.mjs +63 -34
  57. package/src/cli/remember.mjs +3 -3
  58. package/src/cli/secret.mjs +812 -0
  59. package/src/cli/setup.mjs +34 -7
  60. package/src/cli/share.mjs +2 -2
  61. package/src/cli/skill.mjs +396 -35
  62. package/src/cli/sync.mjs +4 -4
  63. package/src/cli/task.mjs +287 -0
  64. package/src/core/clarity-rewriter.mjs +5 -5
  65. package/src/core/config.mjs +44 -2
  66. package/src/core/conflict-detector.mjs +5 -5
  67. package/src/core/crons.mjs +54 -0
  68. package/src/core/daemon-loop.mjs +87 -235
  69. package/src/core/dream.mjs +170 -10
  70. package/src/core/env-import.mjs +456 -0
  71. package/src/core/fact-seeker.mjs +14 -14
  72. package/src/core/import-rules.mjs +20 -7
  73. package/src/core/inference-engine.mjs +3 -3
  74. package/src/core/migrate.mjs +14 -8
  75. package/src/core/okf-adapter.mjs +2 -2
  76. package/src/core/operation-validator.mjs +39 -331
  77. package/src/core/optimizer.mjs +7 -0
  78. package/src/core/post-mortem.mjs +2 -2
  79. package/src/core/project-brain.mjs +349 -0
  80. package/src/core/provider-catalog.mjs +296 -0
  81. package/src/core/quick-capture.mjs +1 -1
  82. package/src/core/remote-vault-sync.mjs +194 -0
  83. package/src/core/research-queue.mjs +7 -11
  84. package/src/core/runtime.mjs +33 -9
  85. package/src/core/scheduler.mjs +59 -5
  86. package/src/core/schema.mjs +14 -0
  87. package/src/core/search.mjs +1 -1
  88. package/src/core/secrets-env-export.mjs +323 -0
  89. package/src/core/secrets-rotate.mjs +245 -0
  90. package/src/core/secrets-store.mjs +645 -0
  91. package/src/core/semantic-index.mjs +3 -3
  92. package/src/core/skills-registry.mjs +1062 -0
  93. package/src/core/source-adapters.mjs +4 -1
  94. package/src/core/ssss-host-extension.mjs +94 -0
  95. package/src/core/ssss-kernel-bridge.mjs +530 -0
  96. package/src/core/surface.mjs +14 -0
  97. package/src/core/task-envelope.mjs +298 -0
  98. package/src/core/task-executors.mjs +600 -0
  99. package/src/core/validated-write.mjs +124 -56
  100. package/src/core/vault.mjs +19 -34
  101. package/src/core/webauthn-store.mjs +272 -0
  102. package/src/server/api.mjs +28 -16
  103. package/src/server/auth.mjs +53 -2
  104. package/src/server/index.mjs +3 -3
  105. package/src/server/keys.mjs +30 -4
  106. package/src/server/rest.mjs +405 -68
  107. package/src/server/route-manifest.json +168 -0
  108. package/src/server/routes/_shared.mjs +40 -1
  109. package/src/server/routes/docs.mjs +260 -0
  110. package/src/server/routes/memory.mjs +145 -32
  111. package/src/server/routes/research.mjs +30 -0
  112. package/src/server/routes/secrets.mjs +470 -0
  113. package/src/server/routes/share.mjs +2 -2
  114. package/src/server/routes/skills.mjs +452 -54
  115. package/src/server/routes/sync.mjs +48 -0
  116. package/src/server/routes/webauthn.mjs +184 -0
  117. package/src/server/update-route-manifest.mjs +39 -0
  118. package/templates/onboarding-interview.md +32 -22
  119. package/templates/openwiki/README.md +10 -0
  120. package/templates/openwiki/architecture.md +5 -0
  121. package/templates/openwiki/ide.md +10 -0
  122. package/templates/openwiki/memory.md +11 -0
  123. package/templates/openwiki/secrets.md +7 -0
  124. package/templates/openwiki/skills.md +7 -0
  125. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
  126. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
  127. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
  128. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
  129. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
  130. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
  131. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
  132. package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
  133. package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
  134. package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
  135. package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
  136. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
  137. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
  138. package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
  139. package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
  140. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
  141. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
  142. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
  143. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
  144. package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
  145. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
  146. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
  147. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
  148. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
  149. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
  150. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
  151. /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
  152. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
  153. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
  154. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/scan-skill.mjs +0 -0
@@ -0,0 +1,470 @@
1
+ /**
2
+ * /api/secrets/* — provider secrets catalog (not dashboard PATs; see /api/keys)
3
+ *
4
+ * SSOT: secrets store. Repo/deploy .env files are projections via export-env.
5
+ *
6
+ * - GET/POST catalog, meta, rotate, usage, import-env (migrate-in only)
7
+ * - POST /api/secrets/export-env write .env from store → path or all projects
8
+ * - GET /api/secrets/export-env dry-run / preview keys for a path
9
+ */
10
+
11
+ import express from 'express';
12
+ import path from 'node:path';
13
+ import { requireAuth, requireScope, verifyStepUpToken } from '../auth.mjs';
14
+ import { badRequest, serverError, notFound, BRAIN_DIR, resolveVaultFromQuery } from './_shared.mjs';
15
+ import {
16
+ listSecretsMeta,
17
+ setSecret,
18
+ getSecret,
19
+ getSecretsCatalog,
20
+ updateSecretMeta,
21
+ rotateSecret,
22
+ listRotationDue,
23
+ recordUsage,
24
+ summarizeUsage,
25
+ summarizeUsageByKey,
26
+ listUsageEvents,
27
+ deleteSecret,
28
+ } from '../../core/secrets-store.mjs';
29
+ import { logger } from '../../core/logger.mjs';
30
+ import { listProviders, getProvider } from '../../core/provider-catalog.mjs';
31
+ import {
32
+ scanEnvSources,
33
+ publicScanResult,
34
+ importEnvSecrets,
35
+ candidatesFromPaste,
36
+ inferProvider,
37
+ isCandidateKey,
38
+ } from '../../core/env-import.mjs';
39
+ import {
40
+ exportEnvToProject,
41
+ exportEnvToRegistry,
42
+ buildEnvProjection,
43
+ buildDeploySecretsPayload,
44
+ } from '../../core/secrets-env-export.mjs';
45
+
46
+ const router = express.Router();
47
+
48
+ /**
49
+ * Provider secrets are a single host SSOT — NOT per project-vault brain.
50
+ * Selecting project:ultrachat for memory/graph must not empty the Secrets page.
51
+ * Repo binding lives in secret metadata (repos[]), not separate secrets.enc files.
52
+ *
53
+ * Override only via TR_SECRETS_BRAIN / TR_BRAIN env (deploy/ops).
54
+ */
55
+ function secretsBrainDir(_req) {
56
+ if (process.env.TR_SECRETS_BRAIN) return process.env.TR_SECRETS_BRAIN;
57
+ return BRAIN_DIR;
58
+ }
59
+
60
+ /** @deprecated alias — all secrets routes use global SSOT */
61
+ function brainDirFromReq(req) {
62
+ return secretsBrainDir(req);
63
+ }
64
+
65
+ router.get('/api/secrets/providers', requireAuth, requireScope('keys:read', 'config:read'), (_req, res) => {
66
+ res.json({ providers: listProviders() });
67
+ });
68
+
69
+ router.get('/api/secrets/usage', requireAuth, requireScope('keys:read', 'config:read'), async (req, res) => {
70
+ try {
71
+ const brainDir = brainDirFromReq(req);
72
+ const days = Math.min(90, Math.max(1, parseInt(req.query.days, 10) || 30));
73
+ const summary = summarizeUsage(brainDir, { days });
74
+ const by = summarizeUsageByKey(brainDir, { days });
75
+ const events = listUsageEvents(brainDir, {
76
+ limit: Math.min(200, parseInt(req.query.limit, 10) || 50),
77
+ key_ref: req.query.key_ref || null,
78
+ days,
79
+ });
80
+ res.json({ summary, ...by, events });
81
+ } catch (err) {
82
+ serverError(res, err);
83
+ }
84
+ });
85
+
86
+ router.post('/api/secrets/usage', requireAuth, requireScope('keys:write', 'config:write'), async (req, res) => {
87
+ try {
88
+ const brainDir = brainDirFromReq(req);
89
+ const body = req.body || {};
90
+ const row = recordUsage(brainDir, {
91
+ provider: body.provider || 'unknown',
92
+ model: body.model,
93
+ input_tokens: body.input_tokens || 0,
94
+ output_tokens: body.output_tokens || 0,
95
+ cost_usd: body.cost_usd ?? body.cost ?? null,
96
+ key_ref: body.key_ref || body.key || null,
97
+ source: body.source || 'api',
98
+ });
99
+ res.status(201).json(row);
100
+ } catch (err) {
101
+ serverError(res, err);
102
+ }
103
+ });
104
+
105
+ router.get('/api/secrets/rotation-due', requireAuth, requireScope('keys:read', 'config:read'), async (req, res) => {
106
+ try {
107
+ const brainDir = brainDirFromReq(req);
108
+ const due = await listRotationDue(brainDir, { autoOnly: req.query.auto === '1' });
109
+ res.json({ due, count: due.length });
110
+ } catch (err) {
111
+ serverError(res, err);
112
+ }
113
+ });
114
+
115
+ router.post('/api/secrets/rotation-due/enqueue', requireAuth, requireScope('keys:write', 'config:write'), async (req, res) => {
116
+ try {
117
+ const brainDir = brainDirFromReq(req);
118
+ const { enqueueRotationDueTasks } = await import('../../core/secrets-rotate.mjs');
119
+ const result = await enqueueRotationDueTasks(brainDir, {
120
+ autoOnly: !!req.body?.autoOnly,
121
+ });
122
+ res.json(result);
123
+ } catch (err) {
124
+ serverError(res, err);
125
+ }
126
+ });
127
+
128
+ router.get('/api/secrets', requireAuth, requireScope('keys:read', 'config:read'), async (req, res) => {
129
+ try {
130
+ const brainDir = brainDirFromReq(req);
131
+ if (req.query.catalog === '0') {
132
+ const keys = await listSecretsMeta(brainDir);
133
+ return res.json({ keys, store: path.join(brainDir, 'config', 'secrets.enc') });
134
+ }
135
+ const catalog = await getSecretsCatalog(brainDir);
136
+ res.json(catalog);
137
+ } catch (err) {
138
+ serverError(res, err);
139
+ }
140
+ });
141
+
142
+ /**
143
+ * Preview or write .env projections from secrets store.
144
+ * Body/query: { path?, all_projects?, dry_run?, filename?, include_global?, keys? }
145
+ */
146
+ router.post('/api/secrets/export-env', requireAuth, requireScope('keys:write', 'config:write'), async (req, res) => {
147
+ try {
148
+ const brainDir = brainDirFromReq(req);
149
+ const body = req.body || {};
150
+ const dryRun = !!body.dry_run || !!body.dryRun;
151
+ const exportOpts = {
152
+ filename: body.filename || '.env',
153
+ example: body.example !== false,
154
+ dryRun,
155
+ includeGlobal: body.include_global !== false && body.includeGlobal !== false,
156
+ keys: Array.isArray(body.keys) ? body.keys : undefined,
157
+ };
158
+
159
+ if (body.all_projects || body.allProjects) {
160
+ const results = await exportEnvToRegistry(brainDir, exportOpts);
161
+ return res.json({ results, store: path.join(brainDir, 'config', 'secrets.enc') });
162
+ }
163
+
164
+ const targetPath = body.path || body.project_path || process.cwd();
165
+ if (body.preview_only || body.previewOnly) {
166
+ const projection = await buildEnvProjection(brainDir, {
167
+ projectPath: targetPath,
168
+ includeGlobal: exportOpts.includeGlobal,
169
+ keys: exportOpts.keys,
170
+ });
171
+ return res.json({
172
+ path: targetPath,
173
+ keys: projection.keys,
174
+ count: projection.count,
175
+ store: projection.store,
176
+ // never return dotenv body with values over API unless explicit
177
+ has_values: false,
178
+ });
179
+ }
180
+
181
+ const result = await exportEnvToProject(brainDir, targetPath, exportOpts);
182
+ res.json({
183
+ ...result,
184
+ // strip any accidental value leakage — only paths and key names
185
+ keys: result.keys,
186
+ count: result.count,
187
+ });
188
+ } catch (err) {
189
+ if (err.message?.includes('not a directory')) return badRequest(res, err.message);
190
+ serverError(res, err);
191
+ }
192
+ });
193
+
194
+ router.get('/api/secrets/export-env', requireAuth, requireScope('keys:read', 'config:read'), async (req, res) => {
195
+ try {
196
+ const brainDir = brainDirFromReq(req);
197
+ const targetPath = req.query.path || process.cwd();
198
+ const projection = await buildEnvProjection(brainDir, {
199
+ projectPath: targetPath,
200
+ includeGlobal: req.query.no_global !== '1',
201
+ });
202
+ res.json({
203
+ path: targetPath,
204
+ keys: projection.keys,
205
+ count: projection.count,
206
+ store: projection.store,
207
+ });
208
+ } catch (err) {
209
+ serverError(res, err);
210
+ }
211
+ });
212
+
213
+ /** Deploy helper: returns dotenv text for host agent (auth required). */
214
+ router.post('/api/secrets/deploy-payload', requireAuth, requireScope('keys:write', 'config:write'), async (req, res) => {
215
+ try {
216
+ const brainDir = brainDirFromReq(req);
217
+ const body = req.body || {};
218
+ const payload = await buildDeploySecretsPayload(brainDir, {
219
+ projectPath: body.path,
220
+ projectSlug: body.project_slug || body.projectSlug,
221
+ includeGlobal: body.include_global !== false,
222
+ keys: body.keys,
223
+ });
224
+ // For deploy automation — values included once under auth
225
+ res.json(payload);
226
+ } catch (err) {
227
+ serverError(res, err);
228
+ }
229
+ });
230
+
231
+ router.get('/api/secrets/scan-env', requireAuth, requireScope('keys:write', 'config:write'), async (req, res) => {
232
+ try {
233
+ const brainDir = brainDirFromReq(req);
234
+ const existing = await listSecretsMeta(brainDir);
235
+ const existingSet = new Set(existing.map((e) => e.key));
236
+ const scan = scanEnvSources({
237
+ brainDir,
238
+ includeProcessEnv: true,
239
+ cwd: process.cwd(),
240
+ });
241
+ res.json(publicScanResult(scan, existingSet));
242
+ } catch (err) {
243
+ serverError(res, err);
244
+ }
245
+ });
246
+
247
+ router.post('/api/secrets/parse-env', requireAuth, requireScope('keys:write', 'config:write'), async (req, res) => {
248
+ try {
249
+ const text = req.body?.text;
250
+ if (!text || typeof text !== 'string') {
251
+ return badRequest(res, 'text is required (paste .env contents)');
252
+ }
253
+ if (text.length > 200_000) {
254
+ return badRequest(res, 'text too large (max 200KB)');
255
+ }
256
+ const brainDir = brainDirFromReq(req);
257
+ const existing = await listSecretsMeta(brainDir);
258
+ const existingSet = new Set(existing.map((e) => e.key));
259
+ const { candidates } = candidatesFromPaste(text);
260
+ res.json({
261
+ candidates: candidates.map((c) => ({
262
+ ...c,
263
+ already_set: existingSet.has(c.key),
264
+ })),
265
+ count: candidates.length,
266
+ });
267
+ } catch (err) {
268
+ serverError(res, err);
269
+ }
270
+ });
271
+
272
+ router.post('/api/secrets/import-env', requireAuth, requireScope('keys:write', 'config:write'), async (req, res) => {
273
+ try {
274
+ const brainDir = brainDirFromReq(req);
275
+ const body = req.body || {};
276
+ const overwrite = !!body.overwrite;
277
+
278
+ if (body.pairs && typeof body.pairs === 'object') {
279
+ let pairs = body.pairs;
280
+ if (Array.isArray(body.keys) && body.keys.length) {
281
+ const filtered = {};
282
+ for (const k of body.keys) {
283
+ if (pairs[k] != null) filtered[k] = pairs[k];
284
+ }
285
+ pairs = filtered;
286
+ }
287
+ const clean = {};
288
+ for (const [k, v] of Object.entries(pairs)) {
289
+ // Pattern-based only — any secret-shaped name, no product API whitelist
290
+ if (isCandidateKey(k) && v) clean[k] = String(v);
291
+ }
292
+ const result = await importEnvSecrets(brainDir, {
293
+ pairs: clean,
294
+ overwrite,
295
+ actor: 'dashboard-import',
296
+ });
297
+ return res.json(result);
298
+ }
299
+
300
+ const result = await importEnvSecrets(brainDir, {
301
+ keys: Array.isArray(body.keys) ? body.keys : undefined,
302
+ all: !!body.all,
303
+ overwrite,
304
+ actor: 'dashboard-import',
305
+ });
306
+ res.json(result);
307
+ } catch (err) {
308
+ serverError(res, err);
309
+ }
310
+ });
311
+
312
+ router.post('/api/secrets', requireAuth, requireScope('keys:write', 'config:write'), async (req, res) => {
313
+ try {
314
+ const body = req.body || {};
315
+ const { key, value } = body;
316
+ if (!key || !value) return badRequest(res, 'key and value are required');
317
+ const brainDir = brainDirFromReq(req);
318
+ const provider = body.provider || inferProvider(key);
319
+ const catalog = getProvider(provider);
320
+ await setSecret(brainDir, key, value, {
321
+ provider,
322
+ scope: body.scope || 'global',
323
+ repos: body.repos,
324
+ subscription_tier: body.subscription_tier,
325
+ monthly_cost_usd: body.monthly_cost_usd,
326
+ monthly_cap_usd: body.monthly_cap_usd ?? catalog?.default_monthly_cap_usd,
327
+ api_docs_url: body.api_docs_url || catalog?.docs_url,
328
+ rotate_every_days: body.rotate_every_days,
329
+ auto_rotate: body.auto_rotate,
330
+ notes: body.notes,
331
+ project_path: body.project_path,
332
+ label: body.label,
333
+ actor: 'dashboard',
334
+ });
335
+ const keys = await listSecretsMeta(brainDir);
336
+ res.status(201).json(keys.find((k) => k.key === key));
337
+ } catch (err) {
338
+ if (err.message?.includes('Invalid')) return badRequest(res, err.message);
339
+ serverError(res, err);
340
+ }
341
+ });
342
+
343
+ router.patch('/api/secrets/:key', requireAuth, requireScope('keys:write', 'config:write'), async (req, res) => {
344
+ try {
345
+ const brainDir = brainDirFromReq(req);
346
+ const row = await updateSecretMeta(brainDir, req.params.key, req.body || {}, {
347
+ actor: 'dashboard',
348
+ });
349
+ if (!row) return notFound(res, `Secret not found: ${req.params.key}`);
350
+ res.json(row);
351
+ } catch (err) {
352
+ if (err.message?.includes('not found')) return notFound(res, err.message);
353
+ serverError(res, err);
354
+ }
355
+ });
356
+
357
+ /**
358
+ * Reveal full secret value — requires short-lived step_up_token from passkey
359
+ * (or password re-entry). Never logged. Audit records key name only.
360
+ */
361
+ router.post(
362
+ '/api/secrets/:key/reveal',
363
+ requireAuth,
364
+ requireScope('keys:read', 'config:read'),
365
+ async (req, res) => {
366
+ try {
367
+ const token =
368
+ req.body?.step_up_token ||
369
+ req.headers['x-tr-step-up'] ||
370
+ req.headers['x-total-recall-step-up'];
371
+ const check = verifyStepUpToken(token, 'secrets:reveal');
372
+ if (!check.ok) {
373
+ return res.status(403).json({
374
+ error: check.error,
375
+ code: 'step_up_required',
376
+ hint: 'Complete passkey prompt or password step-up, then retry reveal.',
377
+ });
378
+ }
379
+
380
+ const brainDir = brainDirFromReq(req);
381
+ const key = req.params.key;
382
+ const got = await getSecret(brainDir, key, {
383
+ action: 'reveal',
384
+ actor: `step-up:${check.payload?.actor || 'dashboard'}`,
385
+ });
386
+ if (!got.found) return notFound(res, `Secret not found: ${key}`);
387
+
388
+ logger.info('secrets', 'Secret revealed (step-up)', {
389
+ key,
390
+ actor: check.payload?.actor || 'dashboard',
391
+ });
392
+
393
+ res.json({
394
+ key,
395
+ value: got.value,
396
+ // client should display briefly; server does not store this response
397
+ revealed_at: new Date().toISOString(),
398
+ step_up_actor: check.payload?.actor || null,
399
+ });
400
+ } catch (err) {
401
+ serverError(res, err);
402
+ }
403
+ },
404
+ );
405
+
406
+ router.post('/api/secrets/:key/rotate', requireAuth, requireScope('keys:write', 'config:write'), async (req, res) => {
407
+ try {
408
+ const body = req.body || {};
409
+ const { value } = body;
410
+ if (!value) return badRequest(res, 'value is required for rotate');
411
+ const brainDir = brainDirFromReq(req);
412
+
413
+ // Default: after rotate, re-project .env to bound repos / all projects when asked
414
+ const wantExport =
415
+ body.export_env === true ||
416
+ body.exportEnv === true ||
417
+ body.export_all === true ||
418
+ body.exportAll === true;
419
+
420
+ if (wantExport) {
421
+ const { rotateSecretAndExport } = await import('../../core/secrets-rotate.mjs');
422
+ const result = await rotateSecretAndExport(brainDir, req.params.key, value, {
423
+ actor: 'dashboard',
424
+ provider: body.provider,
425
+ exportEnv: true,
426
+ exportAllProjects: body.export_all === true || body.exportAll === true,
427
+ exportCwd: false,
428
+ includeGlobal: true,
429
+ });
430
+ // Never echo secret value
431
+ const keys = await listSecretsMeta(brainDir);
432
+ return res.json({
433
+ key: result.key,
434
+ rotated: true,
435
+ next_rotate_due: result.next_rotate_due,
436
+ secret: keys.find((k) => k.key === req.params.key),
437
+ exports: (result.exports || []).map((e) => ({
438
+ ok: e.ok !== false,
439
+ envPath: e.envPath,
440
+ count: e.count,
441
+ name: e.name,
442
+ error: e.error,
443
+ })),
444
+ });
445
+ }
446
+
447
+ const result = await rotateSecret(brainDir, req.params.key, value, {
448
+ actor: 'dashboard',
449
+ provider: body.provider,
450
+ });
451
+ const keys = await listSecretsMeta(brainDir);
452
+ res.json({ ...result, secret: keys.find((k) => k.key === req.params.key) });
453
+ } catch (err) {
454
+ if (err.message?.includes('not found')) return notFound(res, err.message);
455
+ serverError(res, err);
456
+ }
457
+ });
458
+
459
+ router.delete('/api/secrets/:key', requireAuth, requireScope('keys:write', 'config:write'), async (req, res) => {
460
+ try {
461
+ const brainDir = brainDirFromReq(req);
462
+ const r = await deleteSecret(brainDir, req.params.key, { actor: 'dashboard' });
463
+ if (!r.found) return notFound(res, `Secret not found: ${req.params.key}`);
464
+ res.json(r);
465
+ } catch (err) {
466
+ serverError(res, err);
467
+ }
468
+ });
469
+
470
+ export { router as secretsRouter };
@@ -9,7 +9,7 @@
9
9
  import express from 'express';
10
10
  import crypto from 'node:crypto';
11
11
  import { createMemoryNode } from '../../core/vault.mjs';
12
- import { writeNodeValidated } from '../../core/validated-write.mjs';
12
+ import { writeNodeValidatedAsync } from '../../core/validated-write.mjs';
13
13
  import { invalidate } from '../../core/vault-cache.mjs';
14
14
  import { addToQueue } from '../../core/research-queue.mjs';
15
15
  import { requireAuth, requireScope } from '../auth.mjs';
@@ -122,7 +122,7 @@ router.post('/api/share', requireAuth, requireScope('memory:write'), async (req,
122
122
  }];
123
123
  }
124
124
 
125
- const vaultResult = writeNodeValidated(node, vaultDir);
125
+ const vaultResult = await writeNodeValidatedAsync(node, vaultDir);
126
126
  if (!vaultResult.success) {
127
127
  return res.status(422).json({
128
128
  error: 'Validation failed',