total-recall-brain 3.13.0 → 3.13.2
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/README.md +91 -108
- package/bin/antigravity.mjs +81 -52
- package/bin/total-recall.mjs +32 -57
- package/extension/background.js +1 -1
- package/extension/options/options.js +9 -3
- package/extension/sidepanel/sidepanel.js +0 -2
- package/frontend/index.html +4 -3
- package/frontend/package.json +1 -0
- package/frontend/src/App.tsx +109 -152
- package/frontend/src/api.ts +605 -23
- package/frontend/src/components/BrainSelector.tsx +211 -199
- package/frontend/src/components/DocumentEditorModal.tsx +57 -0
- package/frontend/src/components/DocumentTable.tsx +47 -0
- package/frontend/src/components/Graph3D.tsx +120 -71
- package/frontend/src/components/UsageChart.tsx +3 -3
- package/frontend/src/components/brand/BrandMark.tsx +95 -0
- package/frontend/src/index.css +286 -86
- package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
- package/frontend/src/pages/ChatPage.tsx +103 -45
- package/frontend/src/pages/DesignDocsPage.tsx +5 -5
- package/frontend/src/pages/FilesPage.tsx +10 -6
- package/frontend/src/pages/GraphPage.tsx +5 -3
- package/frontend/src/pages/HelpPage.tsx +2 -2
- package/frontend/src/pages/InboxPage.tsx +82 -0
- package/frontend/src/pages/InstructionsPage.tsx +1 -1
- package/frontend/src/pages/IntegrationsPage.tsx +21 -10
- package/frontend/src/pages/LoginPage.tsx +95 -77
- package/frontend/src/pages/MemoryPage.tsx +31 -8
- package/frontend/src/pages/OnboardingPage.tsx +690 -0
- package/frontend/src/pages/OpenWikiPage.tsx +412 -323
- package/frontend/src/pages/SettingsPage.tsx +825 -922
- package/frontend/src/pages/SkillsPage.tsx +272 -147
- package/frontend/src/pages/UsagePage.tsx +23 -31
- package/frontend/src/pages/VaultPage.tsx +223 -0
- package/frontend/src/types.ts +13 -0
- package/package.json +4 -4
- package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
- package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
- package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
- package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
- package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
- package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
- package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
- package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
- package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
- package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
- package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
- package/src/cli/brain.mjs +63 -5
- package/src/cli/connect.mjs +11 -19
- package/src/cli/daemon.mjs +5 -4
- package/src/cli/deploy-ui.mjs +8 -3
- package/src/cli/deploy.mjs +84 -27
- package/src/cli/ingest/index.mjs +1 -1
- package/src/cli/init.mjs +63 -34
- package/src/cli/remember.mjs +3 -3
- package/src/cli/secret.mjs +812 -0
- package/src/cli/setup.mjs +34 -7
- package/src/cli/share.mjs +2 -2
- package/src/cli/skill.mjs +396 -35
- package/src/cli/sync.mjs +4 -4
- package/src/cli/task.mjs +287 -0
- package/src/core/clarity-rewriter.mjs +5 -5
- package/src/core/config.mjs +44 -2
- package/src/core/conflict-detector.mjs +5 -5
- package/src/core/crons.mjs +54 -0
- package/src/core/daemon-loop.mjs +87 -235
- package/src/core/dream.mjs +170 -10
- package/src/core/env-import.mjs +456 -0
- package/src/core/fact-seeker.mjs +14 -14
- package/src/core/import-rules.mjs +20 -7
- package/src/core/inference-engine.mjs +3 -3
- package/src/core/migrate.mjs +14 -8
- package/src/core/okf-adapter.mjs +2 -2
- package/src/core/operation-validator.mjs +39 -331
- package/src/core/optimizer.mjs +7 -0
- package/src/core/post-mortem.mjs +2 -2
- package/src/core/project-brain.mjs +349 -0
- package/src/core/provider-catalog.mjs +296 -0
- package/src/core/quick-capture.mjs +1 -1
- package/src/core/remote-vault-sync.mjs +194 -0
- package/src/core/research-queue.mjs +7 -11
- package/src/core/runtime.mjs +33 -9
- package/src/core/scheduler.mjs +59 -5
- package/src/core/schema.mjs +14 -0
- package/src/core/search.mjs +1 -1
- package/src/core/secrets-env-export.mjs +323 -0
- package/src/core/secrets-rotate.mjs +245 -0
- package/src/core/secrets-store.mjs +645 -0
- package/src/core/semantic-index.mjs +3 -3
- package/src/core/skills-registry.mjs +1062 -0
- package/src/core/source-adapters.mjs +4 -1
- package/src/core/ssss-host-extension.mjs +94 -0
- package/src/core/ssss-kernel-bridge.mjs +530 -0
- package/src/core/surface.mjs +14 -0
- package/src/core/task-envelope.mjs +298 -0
- package/src/core/task-executors.mjs +600 -0
- package/src/core/validated-write.mjs +124 -56
- package/src/core/vault.mjs +21 -34
- package/src/core/webauthn-store.mjs +272 -0
- package/src/server/api.mjs +26 -16
- package/src/server/auth.mjs +51 -0
- package/src/server/index.mjs +3 -3
- package/src/server/keys.mjs +30 -4
- package/src/server/rest.mjs +198 -25
- package/src/server/route-manifest.json +156 -0
- package/src/server/routes/_shared.mjs +40 -1
- package/src/server/routes/docs.mjs +260 -0
- package/src/server/routes/memory.mjs +145 -32
- package/src/server/routes/research.mjs +30 -0
- package/src/server/routes/secrets.mjs +470 -0
- package/src/server/routes/share.mjs +2 -2
- package/src/server/routes/skills.mjs +452 -54
- package/src/server/routes/sync.mjs +48 -0
- package/src/server/routes/webauthn.mjs +184 -0
- package/src/server/update-route-manifest.mjs +39 -0
- package/templates/onboarding-interview.md +32 -22
- package/templates/openwiki/README.md +10 -0
- package/templates/openwiki/architecture.md +5 -0
- package/templates/openwiki/ide.md +10 -0
- package/templates/openwiki/memory.md +11 -0
- package/templates/openwiki/secrets.md +7 -0
- package/templates/openwiki/skills.md +7 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
- /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
- /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 {
|
|
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 =
|
|
125
|
+
const vaultResult = await writeNodeValidatedAsync(node, vaultDir);
|
|
126
126
|
if (!vaultResult.success) {
|
|
127
127
|
return res.status(422).json({
|
|
128
128
|
error: 'Validation failed',
|