total-recall-brain 3.41.3 → 3.41.4
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/bin/total-recall.mjs +10 -1
- package/frontend/dist/assets/{index-DFenQQEQ.js → index-CRzjQLu7.js} +77 -72
- package/frontend/dist/index.html +1 -1
- package/package.json +7 -5
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +48 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/mesh-secrets.mjs +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/secrets.mjs +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-carriers.mjs +76 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-prune.mjs +37 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +53 -17
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +12 -12
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +12 -12
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +23 -8
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +6 -2
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +23 -8
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +23 -10
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/grants.mjs +7 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +61 -38
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +57 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +220 -36
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/server/routes/mesh.mjs +7 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +65 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +534 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +139 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +2849 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +376 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +37 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adapters/dsh.mjs +446 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +398 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +152 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +82 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +90 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +415 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/tracker.mjs +133 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +233 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +845 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/BRICK.md +64 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/index.mjs +250 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +344 -14
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +2917 -140
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +9 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +223 -5
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +129 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +355 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +375 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +35 -22
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +947 -323
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +23 -8
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +5 -1
- package/src/cli/config.mjs +3 -0
- package/src/cli/connect.mjs +12 -116
- package/src/cli/harness.mjs +0 -1
- package/src/cli/plugin/create.mjs +56 -3
- package/src/cli/plugin/index.mjs +15 -3
- package/src/cli/plugin/monetization.mjs +232 -12
- package/src/cli/preflight.mjs +161 -0
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/context-compiler.mjs +8 -1
- package/src/core/default-plugins.mjs +1 -1
- package/src/core/meta-harness.mjs +16 -38
- package/src/core/plugin-dependencies.mjs +6 -1
- package/src/core/preflight-gate.mjs +77 -0
- package/src/core/preflight-receipt.mjs +123 -0
- package/src/core/runtime.mjs +0 -3
- package/src/core/surface.mjs +38 -1
package/frontend/dist/index.html
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
18
18
|
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,800&family=Cabinet+Grotesk:wght@700;800&family=Chivo+Mono:wght@500;700&family=Cinzel:wght@600;700;900&family=Cormorant+Garamond:ital,wght@0,600;0,700;1,600;1,700&family=DM+Mono:wght@500&family=DM+Sans:wght@500;700&family=Fraunces:opsz,wght@9..144,600;9..144,800&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&family=Lexend:wght@600;800&family=Newsreader:ital,opsz,wght@0,6..72,600;1,6..72,600&family=Orbitron:wght@700;900&family=Outfit:wght@500;700;800&family=Plus+Jakarta+Sans:wght@600;700;800&family=Space+Grotesk:wght@600;700&family=Syne:wght@700;800&display=swap" rel="stylesheet" />
|
|
19
19
|
<title>Total Recall — Portable Memory</title>
|
|
20
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
+
<script type="module" crossorigin src="/assets/index-CRzjQLu7.js"></script>
|
|
21
21
|
<link rel="stylesheet" crossorigin href="/assets/index-BIQmqDKP.css">
|
|
22
22
|
</head>
|
|
23
23
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "total-recall-brain",
|
|
3
|
-
"version": "3.41.
|
|
3
|
+
"version": "3.41.4",
|
|
4
4
|
"description": "The Vibe Coding Brain & Autonomous Software Factory: memory, rules, secrets, and 33 visual plugins automatically managed across all your devices.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
"prepublishOnly": "npm run check:package && npm run check:dist && npm run check:ssss-registry && node scripts/check-scaffold-state.mjs",
|
|
57
57
|
"check:ssss-registry": "node scripts/check-ssss-registry.mjs",
|
|
58
58
|
"check:package": "node scripts/check-memory-package.mjs",
|
|
59
|
-
"start:memory": "node src/server/memory-server.mjs"
|
|
59
|
+
"start:memory": "node src/server/memory-server.mjs",
|
|
60
|
+
"ssss": "ssss"
|
|
60
61
|
},
|
|
61
62
|
"repository": {
|
|
62
63
|
"type": "git",
|
|
@@ -76,7 +77,7 @@
|
|
|
76
77
|
"homepage": "https://github.com/gregiteen/total-recall#readme",
|
|
77
78
|
"dependencies": {
|
|
78
79
|
"@simplewebauthn/server": "^13.3.2",
|
|
79
|
-
"@ssss/cli": "npm:@gregiteen/ssss-cli@0.
|
|
80
|
+
"@ssss/cli": "npm:@gregiteen/ssss-cli@0.14.3",
|
|
80
81
|
"bcrypt": "^6.0.0",
|
|
81
82
|
"better-sqlite3": "^12.11.1",
|
|
82
83
|
"cookie-parser": "^1.4.7",
|
|
@@ -88,6 +89,7 @@
|
|
|
88
89
|
"jsonwebtoken": "^9.0.3",
|
|
89
90
|
"lodash-es": "^4.18.1",
|
|
90
91
|
"natural": "^8.1.1",
|
|
92
|
+
"openwiki": "^0.7.3",
|
|
91
93
|
"preact": "^10.29.7",
|
|
92
94
|
"sqlite-vss": "^0.1.2",
|
|
93
95
|
"ws": "^8.18.0",
|
|
@@ -103,8 +105,8 @@
|
|
|
103
105
|
"@vitest/coverage-v8": "^4.1.12",
|
|
104
106
|
"gray-matter": "^4.0.3",
|
|
105
107
|
"jsdom": "^29.1.1",
|
|
106
|
-
"react": "^19.
|
|
107
|
-
"react-dom": "^19.
|
|
108
|
+
"react": "^19.3.1",
|
|
109
|
+
"react-dom": "^19.3.1",
|
|
108
110
|
"react-router-dom": "^7.18.1",
|
|
109
111
|
"recharts": "^3.9.2",
|
|
110
112
|
"skills": "^1.5.7",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tr-plugin-auth",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "tr-plugin-auth",
|
|
9
|
-
"version": "1.2.
|
|
9
|
+
"version": "1.2.5",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@simplewebauthn/server": "^13.3.2",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
|
|
3
3
|
"id": "auth",
|
|
4
4
|
"name": "Auth & Secrets",
|
|
5
|
-
"version": "1.2.
|
|
5
|
+
"version": "1.2.5",
|
|
6
6
|
"description": "Hardware WebAuthn biometric FIDO2 passkeys, OAuth session security, and access control.",
|
|
7
7
|
"author": "Total Recall contributors",
|
|
8
8
|
"license": "MIT",
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
enqueueRotationDueTasks,
|
|
52
52
|
getBrowserRotateAssist,
|
|
53
53
|
} from './secrets-rotate.mjs';
|
|
54
|
+
import { findAmbientPasswordExports, removeAmbientPasswordExports, carrierPassword } from './secrets-carriers.mjs';
|
|
54
55
|
import {
|
|
55
56
|
loadRemoteTargets,
|
|
56
57
|
addRemoteTarget,
|
|
@@ -200,6 +201,7 @@ function printHelp() {
|
|
|
200
201
|
generate <key> Generate a random secret for outside integrations
|
|
201
202
|
get <key> [--json] Read selected-store value (audited); JSON reports found/value
|
|
202
203
|
list List keys + metadata only (no values)
|
|
204
|
+
prune Read-only report: unbound, errored and duplicate keys
|
|
203
205
|
rotate <key> Replace value and mark rotated. Prompts for the value;
|
|
204
206
|
--stdin reads fd0, --from-clipboard reads the OS
|
|
205
207
|
clipboard (use the browser you are already signed
|
|
@@ -286,6 +288,9 @@ function printHelp() {
|
|
|
286
288
|
--headscale-url <https-url>
|
|
287
289
|
rotation-due List keys overdue for rotation
|
|
288
290
|
--enqueue Create daemon tasks + browser-use prompts for each due key
|
|
291
|
+
carriers Find shell profiles and /etc files that export the master
|
|
292
|
+
password to every process; --remove-ambient deletes them
|
|
293
|
+
once an owner-only carrier holds the same password.
|
|
289
294
|
fingerprint Identify a credential without printing it: length, type
|
|
290
295
|
prefix, fingerprint and the store keys holding the same
|
|
291
296
|
value. --from-clipboard | --stdin; --expect-prefix <p>
|
|
@@ -643,6 +648,9 @@ function parseArgs(args) {
|
|
|
643
648
|
case '--stdin':
|
|
644
649
|
out.stdin = true;
|
|
645
650
|
break;
|
|
651
|
+
case '--remove-ambient':
|
|
652
|
+
out.removeAmbient = true;
|
|
653
|
+
break;
|
|
646
654
|
case '--app':
|
|
647
655
|
out.app = args[++i];
|
|
648
656
|
break;
|
|
@@ -1045,6 +1053,18 @@ export default async function secretCli(argv) {
|
|
|
1045
1053
|
return;
|
|
1046
1054
|
}
|
|
1047
1055
|
|
|
1056
|
+
if (opts.command === 'prune') {
|
|
1057
|
+
const { pruneCandidates, pruneSummary } = await import('./secrets-prune.mjs');
|
|
1058
|
+
const rows = (await listSecretsMeta(brainDir)).filter((r) => r.set);
|
|
1059
|
+
const candidates = pruneCandidates(rows);
|
|
1060
|
+
const sum = pruneSummary(rows, candidates);
|
|
1061
|
+
console.log(`\n prune report (read-only): ${sum.candidates} of ${sum.total} keys worth a look`);
|
|
1062
|
+
console.log(` unbound=${sum.unbound} error=${sum.errored} duplicated=${sum.duplicated}\n`);
|
|
1063
|
+
for (const c of candidates) console.log(` • ${c.key.padEnd(38)} ${c.provider || '-'} [${c.reasons.join('; ')}]`);
|
|
1064
|
+
console.log('\n Nothing was deleted. Remove a key with: auth secret delete <key>\n');
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1048
1068
|
if (opts.command === 'shared' || opts.command === 'shared-values' || opts.command === 'duplicates') {
|
|
1049
1069
|
const shared = await getSharedValueHealth(brainDir);
|
|
1050
1070
|
console.log(`\n ${shared.healthy ? '✅' : '❌'} ${shared.message}`);
|
|
@@ -1209,6 +1229,34 @@ export default async function secretCli(argv) {
|
|
|
1209
1229
|
return;
|
|
1210
1230
|
}
|
|
1211
1231
|
|
|
1232
|
+
if (opts.command === 'carriers') {
|
|
1233
|
+
// Files that make the master password ambient (shell profiles, /etc/environment).
|
|
1234
|
+
// Reports files and line numbers only; --remove-ambient deletes those exports once an
|
|
1235
|
+
// owner-only carrier is verified to hold the same password.
|
|
1236
|
+
if (opts.removeAmbient) {
|
|
1237
|
+
const r = removeAmbientPasswordExports();
|
|
1238
|
+
if (opts.json) console.log(JSON.stringify(r, null, 2));
|
|
1239
|
+
else {
|
|
1240
|
+
console.log(`\n ${r.removed.length ? '✅ Removed ambient master-password exports' : 'No ambient master-password exports found'}`);
|
|
1241
|
+
for (const row of r.removed) console.log(` ${row.file}: line ${row.lines.join(', ')}`);
|
|
1242
|
+
console.log('');
|
|
1243
|
+
}
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const found = findAmbientPasswordExports();
|
|
1247
|
+
const carried = Boolean(carrierPassword());
|
|
1248
|
+
if (opts.json) console.log(JSON.stringify({ ambient: found, owner_only_carrier: carried }, null, 2));
|
|
1249
|
+
else {
|
|
1250
|
+
console.log(`\n Owner-only carrier holds the master password: ${carried ? 'yes' : 'NO'}`);
|
|
1251
|
+
if (!found.length) console.log(' No ambient exports in shell profiles or /etc');
|
|
1252
|
+
for (const row of found) console.log(` ❌ ${row.file}: line ${row.lines.join(', ')} exports it to every process started there`);
|
|
1253
|
+
if (found.length) console.log(' Remove with: total-recall auth secret carriers --remove-ambient');
|
|
1254
|
+
console.log('');
|
|
1255
|
+
}
|
|
1256
|
+
if (found.length) process.exitCode = 1;
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1212
1260
|
if (opts.command === 'fingerprint') {
|
|
1213
1261
|
// Identify a credential without seeing it: which store keys hold the same
|
|
1214
1262
|
// value, its length and its non-secret type prefix. Lets an operator check
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool logins derived from stored secrets.
|
|
3
|
+
*
|
|
4
|
+
* A secret that reaches a node (see secrets-sync.mjs) is only useful to people and
|
|
5
|
+
* agents there once the tool that uses it is signed in. This signs `gh` in from
|
|
6
|
+
* `github_token`, and wires git to it, so a token rotated once lands on every machine
|
|
7
|
+
* without anyone logging in by hand. Nothing here prints or logs the token.
|
|
8
|
+
*/
|
|
9
|
+
import { execFile } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
const TIMEOUT_MS = 20_000;
|
|
12
|
+
|
|
13
|
+
/** Run a command, optionally feeding stdin; never rejects. */
|
|
14
|
+
export function defaultRun(cmd, args, input) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const child = execFile(cmd, args, { timeout: TIMEOUT_MS, env: { ...process.env, GH_PROMPT_DISABLED: '1', GH_NO_UPDATE_NOTIFIER: '1' } }, (error, stdout, stderr) => {
|
|
17
|
+
resolve({ ok: !error, code: error?.code ?? 0, stdout: String(stdout || ''), stderr: String(stderr || '') });
|
|
18
|
+
});
|
|
19
|
+
child.stdin?.end(input === undefined ? undefined : String(input));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const firstLine = (text) => String(text || '').split('\n').map((l) => l.trim()).find(Boolean) || 'unknown error';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sign `gh` in with `token` when it is not already, then point git at it.
|
|
27
|
+
* @returns {{ status: 'no-token'|'gh-missing'|'current'|'logged-in'|'failed', error?: string }}
|
|
28
|
+
*/
|
|
29
|
+
export async function projectGithubLogin({ token, run = defaultRun } = {}) {
|
|
30
|
+
const wanted = String(token || '').trim();
|
|
31
|
+
if (!wanted) return { status: 'no-token' };
|
|
32
|
+
|
|
33
|
+
const current = await run('gh', ['auth', 'token']);
|
|
34
|
+
if (current.code === 'ENOENT') return { status: 'gh-missing' };
|
|
35
|
+
if (current.ok && current.stdout.trim() === wanted) return { status: 'current' };
|
|
36
|
+
|
|
37
|
+
const login = await run('gh', ['auth', 'login', '--hostname', 'github.com', '--git-protocol', 'https', '--with-token'], wanted);
|
|
38
|
+
if (!login.ok) return { status: 'failed', error: firstLine(login.stderr).replaceAll(wanted, '***') };
|
|
39
|
+
|
|
40
|
+
await run('gh', ['auth', 'setup-git', '--hostname', 'github.com']);
|
|
41
|
+
return { status: 'logged-in' };
|
|
42
|
+
}
|
|
@@ -26,6 +26,24 @@ const FETCH_TIMEOUT_MS = 8_000;
|
|
|
26
26
|
|
|
27
27
|
/* ---------- leader side ---------- */
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* A grant key `repo:<name>` stands for every secret whose `repos` metadata names that
|
|
31
|
+
* repository, so a machine is given the keys its repositories use rather than a list of
|
|
32
|
+
* names or `*`. Resolved on each request, so rebinding a key takes effect on the next sync.
|
|
33
|
+
*/
|
|
34
|
+
export function expandRepoGrants(policy, rows) {
|
|
35
|
+
const byRepo = new Map();
|
|
36
|
+
for (const row of rows || []) for (const repo of row.repos || []) {
|
|
37
|
+
if (!byRepo.has(repo)) byRepo.set(repo, []);
|
|
38
|
+
byRepo.get(repo).push(row.key);
|
|
39
|
+
}
|
|
40
|
+
const grants = (policy?.grants || []).map((grant) => ({
|
|
41
|
+
...grant,
|
|
42
|
+
keys: (grant.keys || []).flatMap((key) => (String(key).startsWith('repo:') ? (byRepo.get(String(key).slice(5)) || []) : [key])),
|
|
43
|
+
}));
|
|
44
|
+
return { ...policy, grants };
|
|
45
|
+
}
|
|
46
|
+
|
|
29
47
|
function audit(brainDir, event) {
|
|
30
48
|
try {
|
|
31
49
|
appendAudit(brainDir, event);
|
|
@@ -26,7 +26,8 @@ import {
|
|
|
26
26
|
} from '../secrets-store.mjs';
|
|
27
27
|
import { logger } from '../logger.mjs';
|
|
28
28
|
import { getMeshSyncAuthorization, requireMeshSyncAuth } from '../mesh-auth.mjs';
|
|
29
|
-
import { createMeshIdentityMiddleware, createMeshListHandler, createMeshSecretHandler } from '../mesh-secrets.mjs';
|
|
29
|
+
import { createMeshIdentityMiddleware, createMeshListHandler, createMeshSecretHandler, expandRepoGrants } from '../mesh-secrets.mjs';
|
|
30
|
+
import { loadPolicy } from '../mesh-identity.mjs';
|
|
30
31
|
import { throttledFetch } from '../throttled-fetch.mjs';
|
|
31
32
|
import { listProviders, getProvider } from '../provider-catalog.mjs';
|
|
32
33
|
import {
|
|
@@ -90,15 +91,17 @@ router.get('/api/secrets/checksum', requireMeshSyncAuth, async (req, res) => {
|
|
|
90
91
|
// Mesh-identity secrets: the caller is the enrolled machine, not a shared
|
|
91
92
|
// token. Each node gets only the keys its grant allows; see mesh-secrets.mjs.
|
|
92
93
|
const requireMeshIdentity = createMeshIdentityMiddleware();
|
|
94
|
+
const meshPolicyFor = async (brainDir) => expandRepoGrants(await loadPolicy(brainDir), await listSecretsMeta(brainDir));
|
|
93
95
|
router.get(
|
|
94
96
|
'/api/secrets/mesh',
|
|
95
97
|
requireMeshIdentity,
|
|
96
98
|
createMeshListHandler({
|
|
97
99
|
brainDir: BRAIN_DIR,
|
|
100
|
+
policyFor: meshPolicyFor,
|
|
98
101
|
keysFor: async () => (await listSecretsMeta(BRAIN_DIR)).map((entry) => entry.key),
|
|
99
102
|
}),
|
|
100
103
|
);
|
|
101
|
-
router.get('/api/secrets/mesh/:key', requireMeshIdentity, createMeshSecretHandler({ brainDir: BRAIN_DIR }));
|
|
104
|
+
router.get('/api/secrets/mesh/:key', requireMeshIdentity, createMeshSecretHandler({ brainDir: BRAIN_DIR, policyFor: meshPolicyFor }));
|
|
102
105
|
|
|
103
106
|
router.get('/api/secrets/list', requireAuth, requireScope('keys:read', 'config:read'), async (req, res) => {
|
|
104
107
|
try {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the secrets-store master password lives on this host.
|
|
3
|
+
*
|
|
4
|
+
* The sanctioned carriers are owner-only: the macOS Keychain, the launchd
|
|
5
|
+
* agent plist and `~/.agent/tr.env`. An `export TR_SECRETS_PASSWORD=…` in a
|
|
6
|
+
* shell profile or in /etc/environment is ambient: every process started from
|
|
7
|
+
* a shell (or, for /etc/environment, every login) inherits it, and a process
|
|
8
|
+
* manager such as pm2 then captures it into each app it starts. This module
|
|
9
|
+
* finds those exports and removes them once a sanctioned carrier holds the
|
|
10
|
+
* same password. It reports files and line numbers, never values.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { secretsPassword } from './secrets-gateway.mjs';
|
|
16
|
+
|
|
17
|
+
const EXPORT = /^\s*(?:export\s+)?(?:TR_SECRETS_PASSWORD|TR_MASTER_PASSWORD)\s*=/;
|
|
18
|
+
const PROFILES = ['.bashrc', '.bash_profile', '.profile', '.zshrc', '.zprofile', '.zshenv'];
|
|
19
|
+
|
|
20
|
+
/** Shell and system files that can make the password ambient. */
|
|
21
|
+
export function ambientCandidateFiles({ home = os.homedir(), etc = '/etc' } = {}) {
|
|
22
|
+
const files = PROFILES.map((name) => path.join(home, name));
|
|
23
|
+
files.push(path.join(etc, 'environment'), path.join(etc, 'profile'), path.join(etc, 'zshenv'));
|
|
24
|
+
try {
|
|
25
|
+
for (const name of fs.readdirSync(path.join(etc, 'profile.d'))) {
|
|
26
|
+
if (name.endsWith('.sh')) files.push(path.join(etc, 'profile.d', name));
|
|
27
|
+
}
|
|
28
|
+
} catch { /* no profile.d */ }
|
|
29
|
+
return files;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Lines that export the master password, by file. */
|
|
33
|
+
export function findAmbientPasswordExports(files = ambientCandidateFiles()) {
|
|
34
|
+
const found = [];
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
let text;
|
|
37
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { continue; }
|
|
38
|
+
const lines = text.split('\n').map((line, index) => (EXPORT.test(line) ? index + 1 : 0)).filter(Boolean);
|
|
39
|
+
if (lines.length) found.push({ file, lines });
|
|
40
|
+
}
|
|
41
|
+
return found;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The password a sanctioned carrier returns with the environment ignored. */
|
|
45
|
+
export function carrierPassword(env = process.env, resolve = secretsPassword) {
|
|
46
|
+
const quiet = { ...env };
|
|
47
|
+
delete quiet.TR_SECRETS_PASSWORD;
|
|
48
|
+
delete quiet.TR_MASTER_PASSWORD;
|
|
49
|
+
return resolve({ env: quiet }) || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Remove ambient exports, only when a sanctioned carrier already returns the
|
|
54
|
+
* password this process was given; otherwise the store would be locked out.
|
|
55
|
+
*
|
|
56
|
+
* @param {{ files?: string[], env?: object, resolve?: Function }} [opts]
|
|
57
|
+
*/
|
|
58
|
+
export function removeAmbientPasswordExports({ files = ambientCandidateFiles(), env = process.env, resolve = secretsPassword } = {}) {
|
|
59
|
+
const exports = findAmbientPasswordExports(files);
|
|
60
|
+
if (!exports.length) return { removed: [], carrier: 'not-checked' };
|
|
61
|
+
const current = env.TR_SECRETS_PASSWORD || env.TR_MASTER_PASSWORD;
|
|
62
|
+
const carried = carrierPassword(env, resolve);
|
|
63
|
+
if (!carried) throw new Error('No owner-only carrier (Keychain, launchd agent or ~/.agent/tr.env) holds the master password; set one with `auth secret rekey --env-file|--keychain` before removing ambient exports');
|
|
64
|
+
if (current && carried !== current) throw new Error('The owner-only carrier holds a different password than this environment; reconcile them before removing ambient exports');
|
|
65
|
+
const removed = [];
|
|
66
|
+
for (const { file, lines } of exports) {
|
|
67
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
68
|
+
const kept = text.split('\n').filter((line) => !EXPORT.test(line)).join('\n');
|
|
69
|
+
const mode = fs.statSync(file).mode & 0o777;
|
|
70
|
+
const tmp = `${file}.tr-tmp-${process.pid}`;
|
|
71
|
+
fs.writeFileSync(tmp, kept, { mode });
|
|
72
|
+
fs.renameSync(tmp, file);
|
|
73
|
+
removed.push({ file, lines });
|
|
74
|
+
}
|
|
75
|
+
return { removed, carrier: 'verified' };
|
|
76
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `auth secret prune`: a read-only report of secrets worth removing or consolidating.
|
|
3
|
+
*
|
|
4
|
+
* A key is a candidate when it is bound to no repository, its provider tracking reports an
|
|
5
|
+
* error (usually a dead key), or it shares a value with another key. Nothing is deleted;
|
|
6
|
+
* the report says why each key is listed so the owner decides.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** @param {Array<{key:string, repos?:string[], tracking_status?:string|null, tracking_exempt?:boolean, fingerprint_full?:string|null, set?:boolean}>} rows */
|
|
10
|
+
export function pruneCandidates(rows) {
|
|
11
|
+
const byFingerprint = new Map();
|
|
12
|
+
for (const row of rows) {
|
|
13
|
+
if (!row.set || !row.fingerprint_full) continue;
|
|
14
|
+
if (!byFingerprint.has(row.fingerprint_full)) byFingerprint.set(row.fingerprint_full, []);
|
|
15
|
+
byFingerprint.get(row.fingerprint_full).push(row.key);
|
|
16
|
+
}
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
const reasons = [];
|
|
20
|
+
if (!(row.repos || []).length) reasons.push('unbound');
|
|
21
|
+
if (row.tracking_status === 'error' && !row.tracking_exempt) reasons.push('error');
|
|
22
|
+
const twins = (byFingerprint.get(row.fingerprint_full) || []).filter((key) => key !== row.key);
|
|
23
|
+
if (twins.length) reasons.push(`same value as ${twins.join(', ')}`);
|
|
24
|
+
if (reasons.length) out.push({ key: row.key, reasons, provider: row.provider || null });
|
|
25
|
+
}
|
|
26
|
+
return out.sort((a, b) => b.reasons.length - a.reasons.length || a.key.localeCompare(b.key));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function pruneSummary(rows, candidates) {
|
|
30
|
+
return {
|
|
31
|
+
total: rows.length,
|
|
32
|
+
candidates: candidates.length,
|
|
33
|
+
unbound: candidates.filter((c) => c.reasons.includes('unbound')).length,
|
|
34
|
+
errored: candidates.filter((c) => c.reasons.includes('error')).length,
|
|
35
|
+
duplicated: candidates.filter((c) => c.reasons.some((r) => r.startsWith('same value'))).length,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -4,20 +4,22 @@ import { getLeaderInfo, isLeader } from './leader-election.mjs';
|
|
|
4
4
|
import { logger } from './logger.mjs';
|
|
5
5
|
import { brainDir } from './config.mjs';
|
|
6
6
|
import { resolveSecretsPath, loadSecrets, saveSecrets, ensureMeta, appendAudit, META_KEY } from './secrets-store.mjs';
|
|
7
|
+
import { projectGithubLogin } from './login-projections.mjs';
|
|
7
8
|
|
|
8
9
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
10
|
+
const FETCH_CONCURRENCY = 8;
|
|
9
11
|
|
|
10
12
|
function serverPort() {
|
|
11
13
|
const value = Number(process.env.TR_SERVER_PORT || process.env.PORT || 3000);
|
|
12
14
|
return Number.isInteger(value) && value > 0 && value <= 65535 ? value : 3000;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
function meshUrl(ip, route) {
|
|
17
|
+
function meshUrl(ip, route, port = serverPort()) {
|
|
16
18
|
const octets = String(ip || '').split('.').map(Number);
|
|
17
19
|
if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255) || octets[0] !== 100 || octets[1] < 64 || octets[1] > 127) {
|
|
18
20
|
throw new Error('Leader IP is outside the Tailscale CGNAT range');
|
|
19
21
|
}
|
|
20
|
-
return `http://${ip}:${
|
|
22
|
+
return `http://${ip}:${port}${route}`;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export function getSecretsChecksum() {
|
|
@@ -39,23 +41,42 @@ export function secretsSyncMode(env = process.env) {
|
|
|
39
41
|
* anything. The node's store is never replaced wholesale and the node keeps
|
|
40
42
|
* its own master password.
|
|
41
43
|
*/
|
|
42
|
-
export async function syncGrantedSecrets(leaderIp, { dir = brainDir, fetchImpl = fetch } = {}) {
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
export async function syncGrantedSecrets(leaderIp, { dir = brainDir, fetchImpl = fetch, port } = {}) {
|
|
45
|
+
// The leader's port, never this node's: the droplet serves on 3900 while devices
|
|
46
|
+
// default to 3000, and assuming our own port made every sync fail.
|
|
47
|
+
const base = meshUrl(leaderIp, '/api/secrets/mesh', port || serverPort());
|
|
48
|
+
let listResponse;
|
|
49
|
+
try {
|
|
50
|
+
listResponse = await fetchImpl(base, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// "fetch failed" alone named neither the address nor the reason.
|
|
53
|
+
throw new Error(`No Total Recall server answered at ${base}: ${err.cause?.code || err.message}`);
|
|
54
|
+
}
|
|
45
55
|
if (!listResponse.ok) throw new Error(`Leader returned ${listResponse.status}`);
|
|
46
56
|
const { keys } = await listResponse.json();
|
|
47
57
|
if (!Array.isArray(keys)) throw new Error('Invalid grant list');
|
|
48
58
|
|
|
59
|
+
// Each key costs the leader a store decryption plus a mesh round trip (2-3 s from a laptop),
|
|
60
|
+
// so a few at a time keeps a full sync under a minute instead of several.
|
|
49
61
|
const fetched = {};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
let next = 0;
|
|
63
|
+
const worker = async () => {
|
|
64
|
+
while (next < keys.length) {
|
|
65
|
+
const key = keys[next++];
|
|
66
|
+
const response = await fetchImpl(`${base}/${encodeURIComponent(key)}`, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
67
|
+
if (!response.ok) throw new Error(`Leader returned ${response.status} for ${key}`);
|
|
68
|
+
fetched[key] = (await response.json()).value;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
await Promise.all(Array.from({ length: Math.min(FETCH_CONCURRENCY, keys.length) }, worker));
|
|
55
72
|
|
|
56
73
|
const secrets = ensureMeta(await loadSecrets(dir));
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
// Only keys this sync created may be removed when their grant is revoked. A key the node
|
|
75
|
+
// already held before it was ever granted is the node's own: revoking the grant must not
|
|
76
|
+
// delete it (on the node that owns the full store, that would wipe its secrets).
|
|
77
|
+
const previouslySynced = Array.isArray(secrets[META_KEY].mesh_synced) ? secrets[META_KEY].mesh_synced : [];
|
|
78
|
+
const created = Object.keys(fetched).filter((key) => !(key in secrets));
|
|
79
|
+
const removed = previouslySynced.filter((key) => !(key in fetched) && key in secrets);
|
|
59
80
|
for (const key of removed) {
|
|
60
81
|
delete secrets[key];
|
|
61
82
|
delete secrets[META_KEY].keys?.[key];
|
|
@@ -66,16 +87,31 @@ export async function syncGrantedSecrets(leaderIp, { dir = brainDir, fetchImpl =
|
|
|
66
87
|
secrets[key] = value;
|
|
67
88
|
}
|
|
68
89
|
secrets[META_KEY].mesh_granted = Object.keys(fetched);
|
|
90
|
+
secrets[META_KEY].mesh_synced = [...new Set([...previouslySynced.filter((key) => key in fetched), ...created])];
|
|
69
91
|
await saveSecrets(dir, secrets);
|
|
70
92
|
appendAudit(dir, { action: 'mesh_grant_sync', key: `(${keys.length} granted, ${removed.length} revoked)`, actor: 'mesh-sync' });
|
|
71
93
|
return { granted: keys.length, updated, removed: removed.length };
|
|
72
94
|
}
|
|
73
95
|
|
|
96
|
+
/** Sign in the tools that use synced secrets (today: gh from github_token). */
|
|
97
|
+
export async function projectLogins({ dir = brainDir, run } = {}) {
|
|
98
|
+
const secrets = await loadSecrets(dir);
|
|
99
|
+
const github = await projectGithubLogin({ token: secrets.github_token || secrets.GITHUB_TOKEN, run });
|
|
100
|
+
if (github.status === 'logged-in') appendAudit(dir, { action: 'login_projection', key: 'github_token -> gh', actor: 'mesh-sync' });
|
|
101
|
+
if (github.status === 'failed') logger.error('secrets-sync', 'gh login from github_token failed', { error: github.error });
|
|
102
|
+
return { github };
|
|
103
|
+
}
|
|
104
|
+
|
|
74
105
|
export async function syncLoop() {
|
|
75
106
|
secretsSyncMode();
|
|
76
|
-
if (await isLeader())
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
if (!(await isLeader())) {
|
|
108
|
+
const leaderInfo = await getLeaderInfo();
|
|
109
|
+
if (leaderInfo?.ip) {
|
|
110
|
+
try { await syncGrantedSecrets(leaderInfo.ip, { port: leaderInfo.port }); }
|
|
111
|
+
catch (err) { logger.error('secrets-sync', 'Failed to sync granted secrets', { error: err.message }); }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Even when the leader is unreachable, the last synced token should still be signed in.
|
|
115
|
+
try { await projectLogins(); }
|
|
116
|
+
catch (err) { logger.error('secrets-sync', 'Login projection failed', { error: err.message }); }
|
|
81
117
|
}
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
"version": "0.2.2",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@gregiteen/ssss-cli": "0.
|
|
12
|
+
"@gregiteen/ssss-cli": "0.14.3"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"total-recall-brain": "3.41.
|
|
15
|
+
"total-recall-brain": "3.41.3"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"node_modules/@gregiteen/ssss-cli": {
|
|
19
|
-
"version": "0.
|
|
20
|
-
"resolved": "https://registry.npmjs.org/@gregiteen/ssss-cli/-/ssss-cli-0.
|
|
21
|
-
"integrity": "sha512-
|
|
19
|
+
"version": "0.14.3",
|
|
20
|
+
"resolved": "https://registry.npmjs.org/@gregiteen/ssss-cli/-/ssss-cli-0.14.3.tgz",
|
|
21
|
+
"integrity": "sha512-vvGgiNLjus3/xp+iotiGqbSG0R7+Q/bP5cP2Psa7Rhtm/vrMuM5B4LebYu7bPf3TzgCyTbv96PcxGWf6gxbW0Q==",
|
|
22
22
|
"license": "ISC",
|
|
23
23
|
"bin": {
|
|
24
24
|
"ssss": "scripts/ssss.mjs"
|
|
@@ -364,9 +364,9 @@
|
|
|
364
364
|
},
|
|
365
365
|
"node_modules/@ssss/cli": {
|
|
366
366
|
"name": "@gregiteen/ssss-cli",
|
|
367
|
-
"version": "0.
|
|
368
|
-
"resolved": "https://registry.npmjs.org/@gregiteen/ssss-cli/-/ssss-cli-0.
|
|
369
|
-
"integrity": "sha512-
|
|
367
|
+
"version": "0.12.1",
|
|
368
|
+
"resolved": "https://registry.npmjs.org/@gregiteen/ssss-cli/-/ssss-cli-0.12.1.tgz",
|
|
369
|
+
"integrity": "sha512-ff0L2LV4db1CfD4W3VmIJwT9fvrpuKDGDfhe6gtjrrgtRmo7BP7jYjRtH7yuQipKikyiP9LIvmVIaxvbjLTwkA==",
|
|
370
370
|
"dev": true,
|
|
371
371
|
"license": "ISC",
|
|
372
372
|
"bin": {
|
|
@@ -2595,9 +2595,9 @@
|
|
|
2595
2595
|
}
|
|
2596
2596
|
},
|
|
2597
2597
|
"node_modules/total-recall-brain": {
|
|
2598
|
-
"version": "3.41.
|
|
2599
|
-
"resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.41.
|
|
2600
|
-
"integrity": "sha512-
|
|
2598
|
+
"version": "3.41.3",
|
|
2599
|
+
"resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.41.3.tgz",
|
|
2600
|
+
"integrity": "sha512-0uIh3WZE3dHxrKFTtRMxvSBLx7aV3j7ry9H9NxRShtCycwQ38T7+70Q/GJAqsdpwt894h8B6AAmWJnHwsFG3XQ==",
|
|
2601
2601
|
"dev": true,
|
|
2602
2602
|
"license": "MIT",
|
|
2603
2603
|
"workspaces": [
|
|
@@ -2605,7 +2605,7 @@
|
|
|
2605
2605
|
],
|
|
2606
2606
|
"dependencies": {
|
|
2607
2607
|
"@simplewebauthn/server": "^13.3.2",
|
|
2608
|
-
"@ssss/cli": "npm:@gregiteen/ssss-cli@0.
|
|
2608
|
+
"@ssss/cli": "npm:@gregiteen/ssss-cli@0.12.1",
|
|
2609
2609
|
"bcrypt": "^6.0.0",
|
|
2610
2610
|
"better-sqlite3": "^12.11.1",
|
|
2611
2611
|
"cookie-parser": "^1.4.7",
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
"version": "0.2.2",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@gregiteen/ssss-cli": "0.
|
|
12
|
+
"@gregiteen/ssss-cli": "0.14.3"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"total-recall-brain": "3.41.
|
|
15
|
+
"total-recall-brain": "3.41.3"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"node_modules/@gregiteen/ssss-cli": {
|
|
19
|
-
"version": "0.
|
|
20
|
-
"resolved": "https://registry.npmjs.org/@gregiteen/ssss-cli/-/ssss-cli-0.
|
|
21
|
-
"integrity": "sha512-
|
|
19
|
+
"version": "0.14.3",
|
|
20
|
+
"resolved": "https://registry.npmjs.org/@gregiteen/ssss-cli/-/ssss-cli-0.14.3.tgz",
|
|
21
|
+
"integrity": "sha512-vvGgiNLjus3/xp+iotiGqbSG0R7+Q/bP5cP2Psa7Rhtm/vrMuM5B4LebYu7bPf3TzgCyTbv96PcxGWf6gxbW0Q==",
|
|
22
22
|
"license": "ISC",
|
|
23
23
|
"bin": {
|
|
24
24
|
"ssss": "scripts/ssss.mjs"
|
|
@@ -364,9 +364,9 @@
|
|
|
364
364
|
},
|
|
365
365
|
"node_modules/@ssss/cli": {
|
|
366
366
|
"name": "@gregiteen/ssss-cli",
|
|
367
|
-
"version": "0.
|
|
368
|
-
"resolved": "https://registry.npmjs.org/@gregiteen/ssss-cli/-/ssss-cli-0.
|
|
369
|
-
"integrity": "sha512-
|
|
367
|
+
"version": "0.12.1",
|
|
368
|
+
"resolved": "https://registry.npmjs.org/@gregiteen/ssss-cli/-/ssss-cli-0.12.1.tgz",
|
|
369
|
+
"integrity": "sha512-ff0L2LV4db1CfD4W3VmIJwT9fvrpuKDGDfhe6gtjrrgtRmo7BP7jYjRtH7yuQipKikyiP9LIvmVIaxvbjLTwkA==",
|
|
370
370
|
"dev": true,
|
|
371
371
|
"license": "ISC",
|
|
372
372
|
"bin": {
|
|
@@ -2595,9 +2595,9 @@
|
|
|
2595
2595
|
}
|
|
2596
2596
|
},
|
|
2597
2597
|
"node_modules/total-recall-brain": {
|
|
2598
|
-
"version": "3.41.
|
|
2599
|
-
"resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.41.
|
|
2600
|
-
"integrity": "sha512-
|
|
2598
|
+
"version": "3.41.3",
|
|
2599
|
+
"resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.41.3.tgz",
|
|
2600
|
+
"integrity": "sha512-0uIh3WZE3dHxrKFTtRMxvSBLx7aV3j7ry9H9NxRShtCycwQ38T7+70Q/GJAqsdpwt894h8B6AAmWJnHwsFG3XQ==",
|
|
2601
2601
|
"dev": true,
|
|
2602
2602
|
"license": "MIT",
|
|
2603
2603
|
"workspaces": [
|
|
@@ -2605,7 +2605,7 @@
|
|
|
2605
2605
|
],
|
|
2606
2606
|
"dependencies": {
|
|
2607
2607
|
"@simplewebauthn/server": "^13.3.2",
|
|
2608
|
-
"@ssss/cli": "npm:@gregiteen/ssss-cli@0.
|
|
2608
|
+
"@ssss/cli": "npm:@gregiteen/ssss-cli@0.12.1",
|
|
2609
2609
|
"bcrypt": "^6.0.0",
|
|
2610
2610
|
"better-sqlite3": "^12.11.1",
|
|
2611
2611
|
"cookie-parser": "^1.4.7",
|