crbro-memory 2.10.0 → 2.11.0
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/LICENSE +21 -21
- package/README.md +256 -465
- package/SECURITY.md +114 -12
- package/bin/crbro.mjs +539 -300
- package/dist/build-id.json +10 -0
- package/dist/cli/common.js +211 -0
- package/dist/cli/export.js +522 -0
- package/dist/cli/import.js +683 -0
- package/dist/cli/markdown.js +313 -0
- package/dist/cli/mcpclient.js +107 -0
- package/dist/cli/serve.js +453 -0
- package/dist/cli/view.js +358 -0
- package/dist/cli/viewer-page.js +378 -0
- package/dist/daemon/daemon.js +119 -11
- package/dist/daemon/endpoint.js +222 -21
- package/dist/daemon/lines.js +102 -13
- package/dist/daemon/proxy.js +480 -125
- package/dist/daemon/surface.js +116 -0
- package/dist/daemon/trace.js +73 -0
- package/dist/engine/atrest.js +132 -0
- package/dist/engine/backup.js +139 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +667 -232
- package/dist/engine/doctor.js +312 -14
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +18 -7
- package/dist/engine/hookrefresh.js +296 -0
- package/dist/engine/hookverify.js +15 -3
- package/dist/engine/installer.js +1022 -0
- package/dist/engine/installio.js +279 -0
- package/dist/engine/keychain.js +721 -162
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +29 -6
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +25 -3
- package/dist/engine/synapses.js +183 -80
- package/dist/engine/triggers.js +141 -11
- package/dist/index.js +37 -3
- package/dist/mcp-surface.json +1 -0
- package/dist/miner/extractor.js +64 -17
- package/dist/miner/scheduler.js +144 -68
- package/dist/search/index.js +797 -176
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +583 -373
- package/dist/sync/git.js +203 -72
- package/dist/sync/space.js +100 -25
- package/dist/sync/validate.js +204 -0
- package/dist/utils/budget.js +15 -2
- package/dist/utils/fs.js +40 -6
- package/dist/utils/ids.js +128 -14
- package/dist/utils/lock.js +94 -7
- package/hooks/crbro-codex.mjs +284 -0
- package/hooks/crbro-guard.mjs +281 -173
- package/hooks/crbro-hooks.shipped.json +20 -0
- package/hooks/crbro-lifecycle.mjs +16 -4
- package/hooks/crbro-subagent.mjs +180 -180
- package/package.json +9 -5
- package/dist/daemon/daemon.d.ts +0 -25
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js.map +0 -1
- package/dist/daemon/endpoint.d.ts +0 -66
- package/dist/daemon/endpoint.d.ts.map +0 -1
- package/dist/daemon/endpoint.js.map +0 -1
- package/dist/daemon/lines.d.ts +0 -55
- package/dist/daemon/lines.d.ts.map +0 -1
- package/dist/daemon/lines.js.map +0 -1
- package/dist/daemon/proxy.d.ts +0 -55
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js.map +0 -1
- package/dist/engine/backup.d.ts +0 -68
- package/dist/engine/backup.d.ts.map +0 -1
- package/dist/engine/backup.js.map +0 -1
- package/dist/engine/brain.d.ts +0 -79
- package/dist/engine/brain.d.ts.map +0 -1
- package/dist/engine/brain.js.map +0 -1
- package/dist/engine/cortex.d.ts +0 -682
- package/dist/engine/cortex.d.ts.map +0 -1
- package/dist/engine/cortex.js.map +0 -1
- package/dist/engine/dates.d.ts +0 -28
- package/dist/engine/dates.d.ts.map +0 -1
- package/dist/engine/dates.js.map +0 -1
- package/dist/engine/doctor.d.ts +0 -93
- package/dist/engine/doctor.d.ts.map +0 -1
- package/dist/engine/doctor.js.map +0 -1
- package/dist/engine/evidence.d.ts +0 -26
- package/dist/engine/evidence.d.ts.map +0 -1
- package/dist/engine/evidence.js.map +0 -1
- package/dist/engine/heat.d.ts +0 -28
- package/dist/engine/heat.d.ts.map +0 -1
- package/dist/engine/heat.js.map +0 -1
- package/dist/engine/hippocampus.d.ts +0 -60
- package/dist/engine/hippocampus.d.ts.map +0 -1
- package/dist/engine/hippocampus.js.map +0 -1
- package/dist/engine/hookverify.d.ts +0 -42
- package/dist/engine/hookverify.d.ts.map +0 -1
- package/dist/engine/hookverify.js.map +0 -1
- package/dist/engine/keychain.d.ts +0 -32
- package/dist/engine/keychain.d.ts.map +0 -1
- package/dist/engine/keychain.js.map +0 -1
- package/dist/engine/maintenance.d.ts +0 -218
- package/dist/engine/maintenance.d.ts.map +0 -1
- package/dist/engine/maintenance.js.map +0 -1
- package/dist/engine/modinstall.d.ts +0 -215
- package/dist/engine/modinstall.d.ts.map +0 -1
- package/dist/engine/modinstall.js.map +0 -1
- package/dist/engine/postmortem.d.ts +0 -81
- package/dist/engine/postmortem.d.ts.map +0 -1
- package/dist/engine/postmortem.js.map +0 -1
- package/dist/engine/prefrontal.d.ts +0 -64
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.js.map +0 -1
- package/dist/engine/projects.d.ts +0 -20
- package/dist/engine/projects.d.ts.map +0 -1
- package/dist/engine/projects.js.map +0 -1
- package/dist/engine/secrets.d.ts +0 -30
- package/dist/engine/secrets.d.ts.map +0 -1
- package/dist/engine/secrets.js.map +0 -1
- package/dist/engine/shelf.d.ts +0 -155
- package/dist/engine/shelf.d.ts.map +0 -1
- package/dist/engine/shelf.js.map +0 -1
- package/dist/engine/source.d.ts +0 -6
- package/dist/engine/source.d.ts.map +0 -1
- package/dist/engine/source.js.map +0 -1
- package/dist/engine/synapses.d.ts +0 -82
- package/dist/engine/synapses.d.ts.map +0 -1
- package/dist/engine/synapses.js.map +0 -1
- package/dist/engine/triggers.d.ts +0 -53
- package/dist/engine/triggers.d.ts.map +0 -1
- package/dist/engine/triggers.js.map +0 -1
- package/dist/engine/usage.d.ts +0 -61
- package/dist/engine/usage.d.ts.map +0 -1
- package/dist/engine/usage.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/miner/extractor.d.ts +0 -12
- package/dist/miner/extractor.d.ts.map +0 -1
- package/dist/miner/extractor.js.map +0 -1
- package/dist/miner/index.d.ts +0 -61
- package/dist/miner/index.d.ts.map +0 -1
- package/dist/miner/index.js.map +0 -1
- package/dist/miner/scheduler.d.ts +0 -30
- package/dist/miner/scheduler.d.ts.map +0 -1
- package/dist/miner/scheduler.js.map +0 -1
- package/dist/miner/types.d.ts +0 -11
- package/dist/miner/types.d.ts.map +0 -1
- package/dist/miner/types.js.map +0 -1
- package/dist/search/index.d.ts +0 -328
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/search/quality.d.ts +0 -18
- package/dist/search/quality.d.ts.map +0 -1
- package/dist/search/quality.js.map +0 -1
- package/dist/search/semantic.d.ts +0 -80
- package/dist/search/semantic.d.ts.map +0 -1
- package/dist/search/semantic.js.map +0 -1
- package/dist/search/synonyms.d.ts +0 -2
- package/dist/search/synonyms.d.ts.map +0 -1
- package/dist/search/synonyms.js.map +0 -1
- package/dist/search/tokenize.d.ts +0 -39
- package/dist/search/tokenize.d.ts.map +0 -1
- package/dist/search/tokenize.js.map +0 -1
- package/dist/server.d.ts +0 -41
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/sync/git.d.ts +0 -36
- package/dist/sync/git.d.ts.map +0 -1
- package/dist/sync/git.js.map +0 -1
- package/dist/sync/materialize.d.ts +0 -36
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -147
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -118
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -369
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -44
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -59
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -20
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/ids.d.ts +0 -47
- package/dist/utils/ids.d.ts.map +0 -1
- package/dist/utils/ids.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -13
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/transcripts.d.ts +0 -63
- package/dist/utils/transcripts.d.ts.map +0 -1
- package/dist/utils/transcripts.js.map +0 -1
- package/dist/version.d.ts +0 -26
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -42,6 +42,7 @@ exports.modPaths = modPaths;
|
|
|
42
42
|
exports.pathListSeparator = pathListSeparator;
|
|
43
43
|
exports.sha256 = sha256;
|
|
44
44
|
exports.modFiles = modFiles;
|
|
45
|
+
exports.staleStaging = staleStaging;
|
|
45
46
|
exports.readModState = readModState;
|
|
46
47
|
exports.compareVersions = compareVersions;
|
|
47
48
|
exports.installMod = installMod;
|
|
@@ -51,13 +52,17 @@ exports.formatModVerify = formatModVerify;
|
|
|
51
52
|
exports.modSwitchedOff = modSwitchedOff;
|
|
52
53
|
exports.takeModNotice = takeModNotice;
|
|
53
54
|
exports.autoInstallMod = autoInstallMod;
|
|
55
|
+
exports.hookRefreshSwitchedOff = hookRefreshSwitchedOff;
|
|
54
56
|
exports.startModOnBoot = startModOnBoot;
|
|
57
|
+
exports.takeHooksNotice = takeHooksNotice;
|
|
55
58
|
exports.modNoticeOnBoot = modNoticeOnBoot;
|
|
56
59
|
exports.resetModOnBoot = resetModOnBoot;
|
|
57
60
|
const crypto_1 = require("crypto");
|
|
58
61
|
const fs_1 = require("fs");
|
|
59
62
|
const os_1 = require("os");
|
|
60
63
|
const path_1 = __importDefault(require("path"));
|
|
64
|
+
const installio_js_1 = require("./installio.js");
|
|
65
|
+
const hookrefresh_js_1 = require("./hookrefresh.js");
|
|
61
66
|
exports.MOD_NAME = 'crbro-pending';
|
|
62
67
|
/** The local copy this mod grew out of: a folder in the list holding it is replaced. */
|
|
63
68
|
exports.LEGACY_NAME = 'crbro-pendientes';
|
|
@@ -69,8 +74,9 @@ exports.PLUGIN_DIRS_VAR = 'CLAUDE_CODE_PLUGIN_DIRS';
|
|
|
69
74
|
/** What the package ships but the install leaves out: tests and editor config. */
|
|
70
75
|
const NOT_SHIPPED_TOP = new Set(['test', 'tsconfig.json', '.gitignore', '.npmignore', 'node_modules']);
|
|
71
76
|
const NOT_SHIPPED_DIRS = new Set(['.claude-plugin/types']);
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
/** Claude Code's folder is CLAUDE_CONFIG_DIR when set, as Claude Code itself reads it; ~/.claude otherwise. */
|
|
78
|
+
function modPaths(home, env = process.env) {
|
|
79
|
+
const claudeDir = (0, installio_js_1.claudeDir)(home, env);
|
|
74
80
|
return {
|
|
75
81
|
claudeDir,
|
|
76
82
|
settingsPath: path_1.default.join(claudeDir, 'settings.json'),
|
|
@@ -212,7 +218,7 @@ function readSettings(settingsPath) {
|
|
|
212
218
|
return { ok: true, settings, indent, eol, hash };
|
|
213
219
|
}
|
|
214
220
|
/** Thrown by writeAtomic when settings.json changed after it was read: the caller merges again. */
|
|
215
|
-
const SETTINGS_CHANGED =
|
|
221
|
+
const SETTINGS_CHANGED = installio_js_1.FILE_CHANGED;
|
|
216
222
|
/** Errors a later start may well not meet (a file held open, an antivirus scan): never written down as a failure. */
|
|
217
223
|
const TRANSIENT = new Set(['EBUSY', 'EPERM', 'EACCES', 'ENOTEMPTY', 'EEXIST', SETTINGS_CHANGED]);
|
|
218
224
|
/**
|
|
@@ -226,43 +232,37 @@ const TRANSIENT = new Set(['EBUSY', 'EPERM', 'EACCES', 'ENOTEMPTY', 'EEXIST', SE
|
|
|
226
232
|
* inherited ACL, and a hard link to settings.json is not kept.
|
|
227
233
|
*/
|
|
228
234
|
function writeAtomic(settingsPath, settings, indent = 2, eol = '\n', expectHash) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
235
|
+
// installio.writeGuarded: the same steps (symlink target, mode, line
|
|
236
|
+
// endings, hash checked again right before the rename), plus a timestamped
|
|
237
|
+
// copy of the file as it was, kept beside it (settings.json.crbro-bak-<stamp>).
|
|
238
|
+
(0, installio_js_1.writeGuarded)(settingsPath, (0, installio_js_1.jsonText)(settings, indent, eol), { expectHash });
|
|
239
|
+
}
|
|
240
|
+
function splitList(value, sep) {
|
|
241
|
+
return (value ?? '').split(sep).map(s => s.trim()).filter(Boolean);
|
|
242
|
+
}
|
|
243
|
+
const STAGING = /^crbro-pending\.(\d+)\.(tmp|old)$/;
|
|
244
|
+
/**
|
|
245
|
+
* A staging or set-aside folder another run left behind, safe to remove: its
|
|
246
|
+
* owner's pid is gone (or it is this process's own). A live pid is another
|
|
247
|
+
* install under way — install-mod by hand beside a boot — and its folder is
|
|
248
|
+
* left alone, or that install would fail half way on its rename.
|
|
249
|
+
*/
|
|
250
|
+
function staleStaging(name, isAlive = pidAlive) {
|
|
251
|
+
const m = STAGING.exec(name);
|
|
252
|
+
if (!m)
|
|
253
|
+
return false;
|
|
254
|
+
const pid = Number(m[1]);
|
|
255
|
+
return pid === process.pid || !isAlive(pid);
|
|
256
|
+
}
|
|
257
|
+
function pidAlive(pid) {
|
|
245
258
|
try {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
text = text.replace(/\n/g, eol);
|
|
249
|
-
(0, fs_1.writeFileSync)(tmp, text, mode === undefined ? 'utf8' : { encoding: 'utf8', mode });
|
|
250
|
-
if (expectHash !== undefined && settingsFingerprint(target) !== expectHash) {
|
|
251
|
-
const changed = new Error(`${settingsPath} changed while it was being written.`);
|
|
252
|
-
changed.code = SETTINGS_CHANGED;
|
|
253
|
-
throw changed;
|
|
254
|
-
}
|
|
255
|
-
(0, fs_1.renameSync)(tmp, target);
|
|
259
|
+
process.kill(pid, 0);
|
|
260
|
+
return true;
|
|
256
261
|
}
|
|
257
262
|
catch (e) {
|
|
258
|
-
|
|
259
|
-
throw e;
|
|
263
|
+
return e.code === 'EPERM';
|
|
260
264
|
}
|
|
261
265
|
}
|
|
262
|
-
function splitList(value, sep) {
|
|
263
|
-
return (value ?? '').split(sep).map(s => s.trim()).filter(Boolean);
|
|
264
|
-
}
|
|
265
|
-
const STAGING = /^crbro-pending\.\d+\.(tmp|old)$/;
|
|
266
266
|
function copyFailed(installedDir, e) {
|
|
267
267
|
const err = new Error(`could not replace ${installedDir} (${e.message}).\n` +
|
|
268
268
|
' Close the sessions and terminals using that folder and run it again.');
|
|
@@ -280,7 +280,7 @@ function copyMod(packageDir, installedDir) {
|
|
|
280
280
|
(0, fs_1.mkdirSync)(parent, { recursive: true });
|
|
281
281
|
// A staging or set-aside folder left by a run that failed half way: its pid is gone.
|
|
282
282
|
for (const name of (0, fs_1.readdirSync)(parent)) {
|
|
283
|
-
if (
|
|
283
|
+
if (staleStaging(name))
|
|
284
284
|
(0, fs_1.rmSync)(path_1.default.join(parent, name), { recursive: true, force: true, maxRetries: 3 });
|
|
285
285
|
}
|
|
286
286
|
const staging = `${installedDir}.${process.pid}.tmp`;
|
|
@@ -326,7 +326,7 @@ async function copyModAsync(packageDir, installedDir) {
|
|
|
326
326
|
const parent = path_1.default.dirname(installedDir);
|
|
327
327
|
await fs_1.promises.mkdir(parent, { recursive: true });
|
|
328
328
|
for (const name of await fs_1.promises.readdir(parent)) {
|
|
329
|
-
if (
|
|
329
|
+
if (staleStaging(name))
|
|
330
330
|
await fs_1.promises.rm(path_1.default.join(parent, name), { recursive: true, force: true, maxRetries: 3 });
|
|
331
331
|
}
|
|
332
332
|
const staging = `${installedDir}.${process.pid}.tmp`;
|
|
@@ -1290,33 +1290,67 @@ async function autoInstallMod(opts) {
|
|
|
1290
1290
|
}
|
|
1291
1291
|
}
|
|
1292
1292
|
let onBoot = null;
|
|
1293
|
+
/** The refresh of installed hook copies (hookrefresh.ts), started beside the mod's install, once per process. */
|
|
1294
|
+
let hooksOnBoot = null;
|
|
1295
|
+
let hooksResult = null;
|
|
1296
|
+
let hooksNoticeTaken = false;
|
|
1297
|
+
/** CRBRO_HOOK_REFRESH=0 (off, false, no) keeps installed hook copies as they are. */
|
|
1298
|
+
function hookRefreshSwitchedOff(env = process.env) {
|
|
1299
|
+
return exports.MOD_OFF_VALUES.has(String(env.CRBRO_HOOK_REFRESH ?? '').trim().toLowerCase());
|
|
1300
|
+
}
|
|
1293
1301
|
/**
|
|
1294
1302
|
* What crbro_boot calls first: starts the automatic install the first time in
|
|
1295
1303
|
* this process and returns the function to call once the boot has its answer
|
|
1296
1304
|
* ready. That function waits for the install until `budgetMs` after the start
|
|
1297
1305
|
* and takes the notice waiting to be said, or null. Neither throws. A boot
|
|
1298
1306
|
* that fails never calls it, so it never takes a notice it cannot hand over.
|
|
1307
|
+
*
|
|
1308
|
+
* It also starts, once per process, the refresh of the hook copies CRBRO
|
|
1309
|
+
* installed (hookrefresh.ts): unmodified copies of an older release are
|
|
1310
|
+
* replaced by this one's. That part does not depend on CRBRO_MOD (it never
|
|
1311
|
+
* adds anything, only keeps current what is there); CRBRO_HOOK_REFRESH=0
|
|
1312
|
+
* turns it off. Its notice is taken with takeHooksNotice().
|
|
1299
1313
|
*/
|
|
1300
1314
|
function startModOnBoot(opts = {}) {
|
|
1301
1315
|
try {
|
|
1302
1316
|
const home = opts.home ?? (0, os_1.homedir)();
|
|
1303
1317
|
const env = opts.env ?? process.env;
|
|
1304
|
-
if (
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1318
|
+
if (!hooksOnBoot && !hookRefreshSwitchedOff(env)) {
|
|
1319
|
+
hooksOnBoot = new Promise(resolve => setImmediate(() => {
|
|
1320
|
+
try {
|
|
1321
|
+
resolve((0, hookrefresh_js_1.refreshHooks)({ home, env }));
|
|
1322
|
+
}
|
|
1323
|
+
catch {
|
|
1324
|
+
resolve([]);
|
|
1325
|
+
}
|
|
1326
|
+
})).then(copies => { hooksResult = copies; return copies; });
|
|
1310
1327
|
}
|
|
1311
|
-
const
|
|
1328
|
+
const hooks = hooksOnBoot ?? Promise.resolve([]);
|
|
1312
1329
|
let timer;
|
|
1313
1330
|
const budget = new Promise(resolve => {
|
|
1314
1331
|
timer = setTimeout(resolve, opts.budgetMs ?? exports.BOOT_BUDGET_MS);
|
|
1315
1332
|
timer.unref?.();
|
|
1316
1333
|
});
|
|
1334
|
+
if (modSwitchedOff(env) || !(0, fs_1.existsSync)(modPaths(home, env).claudeDir)) {
|
|
1335
|
+
return async () => {
|
|
1336
|
+
try {
|
|
1337
|
+
await Promise.race([hooks, budget]);
|
|
1338
|
+
}
|
|
1339
|
+
catch { /* never fails the boot */ }
|
|
1340
|
+
if (timer)
|
|
1341
|
+
clearTimeout(timer);
|
|
1342
|
+
return null;
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
if (!onBoot) {
|
|
1346
|
+
const packageDir = opts.packageDir ?? path_1.default.join(__dirname, '..', '..', 'mods', exports.MOD_NAME);
|
|
1347
|
+
onBoot = autoInstallMod({ ...opts, home, env, packageDir })
|
|
1348
|
+
.catch(e => ({ action: 'failed', reason: e.message }));
|
|
1349
|
+
}
|
|
1350
|
+
const running = onBoot;
|
|
1317
1351
|
return async () => {
|
|
1318
1352
|
try {
|
|
1319
|
-
await Promise.race([running, budget]);
|
|
1353
|
+
await Promise.race([Promise.all([running, hooks]), budget]);
|
|
1320
1354
|
if (timer)
|
|
1321
1355
|
clearTimeout(timer);
|
|
1322
1356
|
return takeModNotice(home, opts.now);
|
|
@@ -1330,6 +1364,22 @@ function startModOnBoot(opts = {}) {
|
|
|
1330
1364
|
return async () => null;
|
|
1331
1365
|
}
|
|
1332
1366
|
}
|
|
1367
|
+
/**
|
|
1368
|
+
* The boot's notice about installed hooks, once per process, if the refresh
|
|
1369
|
+
* has finished: copies left alone because somebody edited them, or a refresh
|
|
1370
|
+
* that failed. Each edited copy is told once (remembered in its manifest).
|
|
1371
|
+
*/
|
|
1372
|
+
function takeHooksNotice() {
|
|
1373
|
+
if (hooksNoticeTaken || !hooksResult)
|
|
1374
|
+
return null;
|
|
1375
|
+
hooksNoticeTaken = true;
|
|
1376
|
+
try {
|
|
1377
|
+
return (0, hookrefresh_js_1.hooksNotice)(hooksResult);
|
|
1378
|
+
}
|
|
1379
|
+
catch {
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1333
1383
|
/** startModOnBoot and its notice in one call. */
|
|
1334
1384
|
async function modNoticeOnBoot(opts = {}) {
|
|
1335
1385
|
return startModOnBoot(opts)();
|
|
@@ -1337,6 +1387,9 @@ async function modNoticeOnBoot(opts = {}) {
|
|
|
1337
1387
|
/** Tests only: the next boot in this process behaves as the first boot of a new process. */
|
|
1338
1388
|
function resetModOnBoot() {
|
|
1339
1389
|
onBoot = null;
|
|
1390
|
+
hooksOnBoot = null;
|
|
1391
|
+
hooksResult = null;
|
|
1392
|
+
hooksNoticeTaken = false;
|
|
1340
1393
|
handedOver.clear();
|
|
1341
1394
|
}
|
|
1342
1395
|
//# sourceMappingURL=modinstall.js.map
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// Active context, hot topics, and global map (clustering + bridges)
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.Prefrontal = void 0;
|
|
6
|
+
exports.sortOpenItems = sortOpenItems;
|
|
6
7
|
const fs_js_1 = require("../utils/fs.js");
|
|
7
8
|
const hash_js_1 = require("../utils/hash.js");
|
|
8
9
|
const secrets_js_1 = require("./secrets.js");
|
|
10
|
+
const heat_js_1 = require("./heat.js");
|
|
9
11
|
/** How many resolved items to keep around as a reminder. */
|
|
10
12
|
const RECENTLY_CLOSED_CAP = 15;
|
|
11
13
|
/** Normalise a stored entry: v1 brains hold plain strings. */
|
|
@@ -22,6 +24,15 @@ function normalizeContext(ctx) {
|
|
|
22
24
|
recently_closed: ctx?.recently_closed || [],
|
|
23
25
|
};
|
|
24
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The one order open items are read in, whatever the scope: what is due
|
|
29
|
+
* first, then the newest. A cap applied after it keeps what matters now; the
|
|
30
|
+
* insertion order kept the oldest, so a boot without a project showed the
|
|
31
|
+
* twelve items nobody had touched in months.
|
|
32
|
+
*/
|
|
33
|
+
function sortOpenItems(items) {
|
|
34
|
+
return [...items].sort((a, b) => (a.due || '9999').localeCompare(b.due || '9999') || String(b.added || '').localeCompare(String(a.added || '')));
|
|
35
|
+
}
|
|
25
36
|
function projectView(ctx, project) {
|
|
26
37
|
if (!project)
|
|
27
38
|
return ctx;
|
|
@@ -29,8 +40,7 @@ function projectView(ctx, project) {
|
|
|
29
40
|
return {
|
|
30
41
|
...ctx, active_topics: ctx.project_topics?.[project] || [],
|
|
31
42
|
project_topics: ctx.project_topics?.[project] ? { [project]: ctx.project_topics[project] } : {},
|
|
32
|
-
pending_tasks: ctx.pending_tasks.map(toTask).filter(visible)
|
|
33
|
-
.sort((a, b) => (a.due || '9999').localeCompare(b.due || '9999') || b.added.localeCompare(a.added)),
|
|
43
|
+
pending_tasks: sortOpenItems(ctx.pending_tasks.map(toTask).filter(visible)),
|
|
34
44
|
recently_closed: (ctx.recently_closed || []).filter(visible),
|
|
35
45
|
};
|
|
36
46
|
}
|
|
@@ -72,6 +82,7 @@ class Prefrontal {
|
|
|
72
82
|
let resolved = [];
|
|
73
83
|
let discarded = [];
|
|
74
84
|
let redacted = [];
|
|
85
|
+
let added;
|
|
75
86
|
const saved = await (0, fs_js_1.updateJSON)(this.brain.paths.activeContext(), current => {
|
|
76
87
|
const ctx = normalizeContext(current);
|
|
77
88
|
if (updates.clear) {
|
|
@@ -113,6 +124,7 @@ class Prefrontal {
|
|
|
113
124
|
Object.assign(existing, metadata);
|
|
114
125
|
else
|
|
115
126
|
tasks.push({ id, text, added: (0, fs_js_1.now)(), status: 'open', ...metadata });
|
|
127
|
+
added = { ...(existing ?? tasks[tasks.length - 1]) };
|
|
116
128
|
}
|
|
117
129
|
// Id, exact text, or a case-insensitive substring of at least 8 chars in
|
|
118
130
|
// either direction — the same predicate for resolving and discarding.
|
|
@@ -142,7 +154,8 @@ class Prefrontal {
|
|
|
142
154
|
ctx.last_updated = (0, fs_js_1.now)();
|
|
143
155
|
return ctx;
|
|
144
156
|
});
|
|
145
|
-
return { ...projectView(saved, updates.project), resolved, discarded, written: true,
|
|
157
|
+
return { ...projectView(saved, updates.project), resolved, discarded, written: true,
|
|
158
|
+
...(redacted.length ? { redacted } : {}), ...(added ? { added } : {}) };
|
|
146
159
|
}
|
|
147
160
|
/**
|
|
148
161
|
* Record which session was logged last. Nothing else in the context moves.
|
|
@@ -181,10 +194,20 @@ class Prefrontal {
|
|
|
181
194
|
async getGlobalMap() {
|
|
182
195
|
const ids = await (0, fs_js_1.listJSONFiles)(this.brain.paths.cortex);
|
|
183
196
|
const neurons = [];
|
|
197
|
+
// Heat lives in the derived heat state since 2.11; a neuron file keeps
|
|
198
|
+
// the value it had when heat stopped being written into it.
|
|
199
|
+
const calor = (await (0, heat_js_1.readHeatState)(this.brain))?.neurons || {};
|
|
184
200
|
for (const id of ids) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
201
|
+
// 2.11 (C1): an unreadable neuron is skipped, not the end of maintenance or the map.
|
|
202
|
+
let neuron = null;
|
|
203
|
+
try {
|
|
204
|
+
neuron = await (0, fs_js_1.readJSON)(this.brain.paths.neuron(id));
|
|
205
|
+
}
|
|
206
|
+
catch { /* corrupt: skipped */ }
|
|
207
|
+
if (!neuron)
|
|
208
|
+
continue;
|
|
209
|
+
const h = calor[neuron.id]?.heat;
|
|
210
|
+
neurons.push(typeof h === 'number' ? { ...neuron, heat: h } : neuron);
|
|
188
211
|
}
|
|
189
212
|
// Build clusters by domain
|
|
190
213
|
const domainGroups = {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Quarantine: the copy kept before anything is removed ────────
|
|
3
|
+
//
|
|
4
|
+
// Every destructive path (forget, forget entire, merge_into, compact, a day
|
|
5
|
+
// log forgotten) writes here first, so a removal can be undone. Until 2.11 it
|
|
6
|
+
// grew without limit and in clear: 824 files and 5.3 MB on the reference
|
|
7
|
+
// brain, a whole 1 MB neuron copied for every batch of a split, including the
|
|
8
|
+
// credentials somebody forgot on purpose. Now:
|
|
9
|
+
//
|
|
10
|
+
// - forgetting SOME entries keeps those entries and what restore needs to
|
|
11
|
+
// put them back (dates, retirements, provenance, checks), not the neuron;
|
|
12
|
+
// - copies older than CRBRO_QUARANTINE_DAYS (default 180; 0 keeps them
|
|
13
|
+
// forever) are pruned by consolidate and maintenance, which say how many;
|
|
14
|
+
// - with CRBRO_ENCRYPT_AT_REST=1 new copies are sealed (atrest.ts).
|
|
15
|
+
//
|
|
16
|
+
// File names: `<id>.<stamp>.json` (a whole neuron or log, the 2.x format a
|
|
17
|
+
// previous version can still restore), `<id>.<stamp>.entries.json` (some
|
|
18
|
+
// entries), each with `.enc` appended when sealed. The stamp sorts lexically,
|
|
19
|
+
// so the name alone orders the copies of one id.
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.DEFAULT_QUARANTINE_DAYS = void 0;
|
|
25
|
+
exports.quarantineDays = quarantineDays;
|
|
26
|
+
exports.stampNow = stampNow;
|
|
27
|
+
exports.writeQuarantine = writeQuarantine;
|
|
28
|
+
exports.readQuarantine = readQuarantine;
|
|
29
|
+
exports.listQuarantine = listQuarantine;
|
|
30
|
+
exports.pruneQuarantine = pruneQuarantine;
|
|
31
|
+
exports.quarantineStats = quarantineStats;
|
|
32
|
+
exports.entriesAsNeuron = entriesAsNeuron;
|
|
33
|
+
exports.isEntriesCopy = isEntriesCopy;
|
|
34
|
+
const node_fs_1 = require("node:fs");
|
|
35
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
36
|
+
const atrest_js_1 = require("./atrest.js");
|
|
37
|
+
const fs_js_1 = require("../utils/fs.js");
|
|
38
|
+
exports.DEFAULT_QUARANTINE_DAYS = 180;
|
|
39
|
+
/** Retention in days from the environment; 0 means forever. Bad values fall back to the default. */
|
|
40
|
+
function quarantineDays(env = process.env) {
|
|
41
|
+
const raw = (env['CRBRO_QUARANTINE_DAYS'] || '').trim();
|
|
42
|
+
if (!raw)
|
|
43
|
+
return exports.DEFAULT_QUARANTINE_DAYS;
|
|
44
|
+
const n = Number(raw);
|
|
45
|
+
return Number.isInteger(n) && n >= 0 ? n : exports.DEFAULT_QUARANTINE_DAYS;
|
|
46
|
+
}
|
|
47
|
+
function stampNow() {
|
|
48
|
+
return (0, fs_js_1.now)().replace(/[:.]/g, '-');
|
|
49
|
+
}
|
|
50
|
+
/** "2026-11-13T10-20-30-123Z" → ISO, or null. */
|
|
51
|
+
function stampToIso(stamp) {
|
|
52
|
+
const m = stamp.match(/^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/);
|
|
53
|
+
return m ? `${m[1]}T${m[2]}:${m[3]}:${m[4]}.${m[5]}Z` : null;
|
|
54
|
+
}
|
|
55
|
+
function parseName(file) {
|
|
56
|
+
const sealed = file.endsWith('.json.enc');
|
|
57
|
+
if (!sealed && !file.endsWith('.json'))
|
|
58
|
+
return null;
|
|
59
|
+
const base = file.slice(0, file.length - (sealed ? '.json.enc'.length : '.json'.length));
|
|
60
|
+
const entries = base.endsWith('.entries');
|
|
61
|
+
const core = entries ? base.slice(0, -'.entries'.length) : base;
|
|
62
|
+
const m = core.match(/^(.*)\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)$/);
|
|
63
|
+
if (!m)
|
|
64
|
+
return { base, id: core, stamp: '', kind: entries ? 'entries' : 'whole', sealed };
|
|
65
|
+
return { base, id: m[1], stamp: m[2], kind: entries ? 'entries' : 'whole', sealed };
|
|
66
|
+
}
|
|
67
|
+
/** Write one copy; sealed when the switch is on. Throws rather than write in clear without a key. */
|
|
68
|
+
async function writeQuarantine(dir, id, data, kind = 'whole') {
|
|
69
|
+
await node_fs_1.promises.mkdir(dir, { recursive: true });
|
|
70
|
+
const { data: bytes, sealed } = (0, atrest_js_1.maybeSeal)(Buffer.from(JSON.stringify(data, null, 2), 'utf8'));
|
|
71
|
+
const name = `${id}.${stampNow()}${kind === 'entries' ? '.entries' : ''}.json${sealed ? '.enc' : ''}`;
|
|
72
|
+
const target = node_path_1.default.join(dir, name);
|
|
73
|
+
const tmp = `${target}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
74
|
+
await node_fs_1.promises.writeFile(tmp, bytes);
|
|
75
|
+
try {
|
|
76
|
+
await node_fs_1.promises.rename(tmp, target);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
await node_fs_1.promises.unlink(tmp).catch(() => { });
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
return target;
|
|
83
|
+
}
|
|
84
|
+
async function readQuarantine(file) {
|
|
85
|
+
let raw;
|
|
86
|
+
try {
|
|
87
|
+
raw = await node_fs_1.promises.readFile(file);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err.code === 'ENOENT')
|
|
91
|
+
return null;
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
return JSON.parse((0, fs_js_1.stripBOM)((0, atrest_js_1.unseal)(raw).toString('utf8')));
|
|
95
|
+
}
|
|
96
|
+
/** Copies in the folder, optionally of one id, newest first. */
|
|
97
|
+
async function listQuarantine(dir, id) {
|
|
98
|
+
let names;
|
|
99
|
+
try {
|
|
100
|
+
names = await node_fs_1.promises.readdir(dir);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const file of names) {
|
|
107
|
+
if (file.endsWith('.tmp'))
|
|
108
|
+
continue;
|
|
109
|
+
const p = parseName(file);
|
|
110
|
+
if (!p)
|
|
111
|
+
continue;
|
|
112
|
+
if (id !== undefined && p.id !== id)
|
|
113
|
+
continue;
|
|
114
|
+
let taken = p.stamp ? stampToIso(p.stamp) : null;
|
|
115
|
+
if (!taken) {
|
|
116
|
+
try {
|
|
117
|
+
taken = (await node_fs_1.promises.stat(node_path_1.default.join(dir, file))).mtime.toISOString();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
out.push({ file, base: p.base, id: p.id, kind: p.kind, sealed: p.sealed, taken });
|
|
124
|
+
}
|
|
125
|
+
return out.sort((a, b) => (a.taken < b.taken ? 1 : a.taken > b.taken ? -1 : a.file < b.file ? 1 : -1));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Delete copies older than `days` (0 = keep forever). Returns what went, or
|
|
129
|
+
* would go with dryRun. Only files this module names are ever touched.
|
|
130
|
+
*/
|
|
131
|
+
async function pruneQuarantine(dir, opts = {}) {
|
|
132
|
+
const days = opts.days ?? quarantineDays();
|
|
133
|
+
const res = { pruned: 0, bytes: 0, files: [], days };
|
|
134
|
+
if (days <= 0)
|
|
135
|
+
return res;
|
|
136
|
+
const limite = (opts.now ?? new Date()).getTime() - days * 86_400_000;
|
|
137
|
+
for (const c of await listQuarantine(dir)) {
|
|
138
|
+
if (Date.parse(c.taken) >= limite)
|
|
139
|
+
continue;
|
|
140
|
+
const f = node_path_1.default.join(dir, c.file);
|
|
141
|
+
try {
|
|
142
|
+
const st = await node_fs_1.promises.stat(f);
|
|
143
|
+
if (!opts.dryRun)
|
|
144
|
+
await node_fs_1.promises.unlink(f);
|
|
145
|
+
res.pruned++;
|
|
146
|
+
res.bytes += st.size;
|
|
147
|
+
res.files.push(c.file);
|
|
148
|
+
}
|
|
149
|
+
catch { /* gone meanwhile */ }
|
|
150
|
+
}
|
|
151
|
+
return res;
|
|
152
|
+
}
|
|
153
|
+
/** Files, bytes and the oldest copy: what status and doctor show. */
|
|
154
|
+
async function quarantineStats(dir) {
|
|
155
|
+
const all = await listQuarantine(dir);
|
|
156
|
+
let bytes = 0;
|
|
157
|
+
for (const c of all) {
|
|
158
|
+
try {
|
|
159
|
+
bytes += (await node_fs_1.promises.stat(node_path_1.default.join(dir, c.file))).size;
|
|
160
|
+
}
|
|
161
|
+
catch { /* gone */ }
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
files: all.length,
|
|
165
|
+
bytes,
|
|
166
|
+
oldest: all.length ? all[all.length - 1].taken : null,
|
|
167
|
+
sealed: all.filter(c => c.sealed).length,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The neuron-shaped part of a partial copy, ready for unionNeuron: the
|
|
172
|
+
* entries with their sidecars and the identity of the neuron they left.
|
|
173
|
+
*/
|
|
174
|
+
function entriesAsNeuron(q) {
|
|
175
|
+
return {
|
|
176
|
+
id: q.neuron_id,
|
|
177
|
+
name: q.name,
|
|
178
|
+
type: q.type,
|
|
179
|
+
domain: q.domain,
|
|
180
|
+
created: q.taken_at,
|
|
181
|
+
last_accessed: q.taken_at,
|
|
182
|
+
access_count: 0,
|
|
183
|
+
heat: 0,
|
|
184
|
+
summary: '',
|
|
185
|
+
facts: q.facts || [],
|
|
186
|
+
decisions: q.decisions || [],
|
|
187
|
+
patterns: q.patterns || [],
|
|
188
|
+
preferences: q.preferences || [],
|
|
189
|
+
connections: [],
|
|
190
|
+
tags: [],
|
|
191
|
+
errors: q.errors || [],
|
|
192
|
+
debts: q.debts || [],
|
|
193
|
+
entry_dates: q.entry_dates || {},
|
|
194
|
+
...(q.entry_status ? { entry_status: q.entry_status } : {}),
|
|
195
|
+
...(q.entry_source ? { entry_source: q.entry_source } : {}),
|
|
196
|
+
...(q.entry_verified ? { entry_verified: q.entry_verified } : {}),
|
|
197
|
+
...(q.entry_verification ? { entry_verification: q.entry_verification } : {}),
|
|
198
|
+
...(q.map ? { map: q.map } : {}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function isEntriesCopy(x) {
|
|
202
|
+
return !!x && typeof x === 'object' && x.kind === 'entries' && x.quarantine_format === 2;
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=quarantine.js.map
|
package/dist/engine/secrets.js
CHANGED
|
@@ -45,7 +45,15 @@ const PATTERNS = [
|
|
|
45
45
|
kind: 'auth token (hex)',
|
|
46
46
|
re: /\bauth[ _-]?token\b[^\S\n]{0,3}(?:[:=]|es|is)?[^\S\n]{0,3}["'`]?([0-9a-fA-F]{30,34})\b/gi,
|
|
47
47
|
},
|
|
48
|
-
|
|
48
|
+
// The whole block, not just its header: the body IS the key. Through the
|
|
49
|
+
// END line when there is one (16 KB is more than any real key); without
|
|
50
|
+
// it, every base64 token that follows (16+ characters, or a padded tail);
|
|
51
|
+
// the first ordinary word stops it. Matching only the header used to
|
|
52
|
+
// leave the body on disk while the benchmark counted it as caught.
|
|
53
|
+
{
|
|
54
|
+
kind: 'private key block',
|
|
55
|
+
re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----(?:[\s\S]{0,16384}?-----END [A-Z0-9 ]*PRIVATE KEY-----|(?:\s{1,4}(?:[A-Za-z0-9+/]{16,}={0,2}|[A-Za-z0-9+/]{2,}={1,2})(?!\S)){0,256})/g,
|
|
56
|
+
},
|
|
49
57
|
{ kind: 'JSON Web Token', re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
|
|
50
58
|
// WordPress application password: six groups of four, anchored to the label.
|
|
51
59
|
//
|
|
@@ -65,8 +73,14 @@ const PATTERNS = [
|
|
|
65
73
|
// copula ("la contraseña es X", "the password is X") with no separator,
|
|
66
74
|
// requiring the value to mix classes so plain words do not match.
|
|
67
75
|
{
|
|
76
|
+
// Three shapes after the separator are prose, not a value, and are left
|
|
77
|
+
// alone (seen on a real brain): a capitalised product name ("Reset
|
|
78
|
+
// contraseña = Supabase Auth built-in"), an assignment ("contraseña:
|
|
79
|
+
// key_filename=~/.ssh/id_x") and a path ("~/.ssh/…", "/etc/…").
|
|
68
80
|
kind: 'password',
|
|
69
|
-
|
|
81
|
+
// No /i flag: the capitalised-word check needs case. The label is
|
|
82
|
+
// spelled in the three casings people write.
|
|
83
|
+
re: /\b(?:[Pp]assword|PASSWORD|[Pp]asswd|PASSWD|[Cc]ontrase[nñ]a|CONTRASE[NÑ]A)\s*(?:es|is|ES|IS)?\s*[:=]\s*["'`]?(?![A-Z][a-z]+(?![^\s"'`,;]))(?![A-Za-z_]+=)(?!~?\/)([^\s"'`,;]{8,})/g,
|
|
70
84
|
},
|
|
71
85
|
{
|
|
72
86
|
// "clave" is also the Spanish adjective "key/important": "Decisión clave:
|
|
@@ -89,6 +103,26 @@ const PATTERNS = [
|
|
|
89
103
|
kind: 'password (prose)',
|
|
90
104
|
re: /\b(?:password|passwd|contrase[nñ]a|clave)\b(?:\s+(?:de|del|of|for)\s+[^\s]{1,30})?\s+(?:es|is)\s+["'`]?((?=[^\s"'`,;]*[A-Za-z])(?=[^\s"'`,;]*[0-9])[^\s"'`,;]{8,})/gi,
|
|
91
105
|
},
|
|
106
|
+
{
|
|
107
|
+
// The label with no separator and no copula, as people really jot it:
|
|
108
|
+
// "PASS Xy7#kk22w", "la contraseña Xy7kk22W (la del panel)". Without a
|
|
109
|
+
// separator the value carries the whole burden of proof, so it must mix
|
|
110
|
+
// three of lower case, upper case, digit and symbol — the rule that keeps
|
|
111
|
+
// "clave" the adjective out (2.9.1). Case-sensitive labels for the same
|
|
112
|
+
// reason. "pass" in lower case is an English verb ("pass userId42 to…"),
|
|
113
|
+
// so it gets its own rule below; "clave" stays out of this one entirely.
|
|
114
|
+
kind: 'password',
|
|
115
|
+
re: /\b(?:PASS|PASSWORD|PASSWD|[Pp]assword|[Pp]asswd|[Cc]ontrase[nñ]a|CONTRASE[NÑ]A)\b[^\S\n]+["'`]?((?:(?=[^\s"'`,;()]*[a-z])(?=[^\s"'`,;()]*[A-Z])(?=[^\s"'`,;()]*[0-9])|(?=[^\s"'`,;()]*[a-z])(?=[^\s"'`,;()]*[A-Z])(?=[^\s"'`,;()]*[!@#$%^&*+=?~])|(?=[^\s"'`,;()]*[a-z])(?=[^\s"'`,;()]*[0-9])(?=[^\s"'`,;()]*[!@#$%^&*+=?~])|(?=[^\s"'`,;()]*[A-Z])(?=[^\s"'`,;()]*[0-9])(?=[^\s"'`,;()]*[!@#$%^&*+=?~]))[^\s"'`,;()]{8,})/g,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
// "pass" as a label: "user admin pass: Xy7kk22w", "pass Xy7#kk22w".
|
|
119
|
+
// With ":"/"=" the value needs a letter and a digit or symbol ("pass:
|
|
120
|
+
// completed" is a test report). Without a separator it needs a letter, a
|
|
121
|
+
// digit AND a symbol, and may not start with $ or - — that is what keeps
|
|
122
|
+
// "pass userId42 to the helper", "pass user_id2" and "pass $TOKEN2" out.
|
|
123
|
+
kind: 'password',
|
|
124
|
+
re: /\b(?:PASS|[Pp]ass)\b(?:[^\S\n]*[:=][^\S\n]*["'`]?((?=[^\s"'`,;()]*[A-Za-z])(?=[^\s"'`,;()]*[0-9!@#$%^&*+=?~])[^\s"'`,;()]{8,})|[^\S\n]+["'`]?(?![$-])((?=[^\s"'`,;()]*[A-Za-z])(?=[^\s"'`,;()]*[0-9])(?=[^\s"'`,;()]*[!@#$%^&*+=?~])[^\s"'`,;()]{8,}))/g,
|
|
125
|
+
},
|
|
92
126
|
{
|
|
93
127
|
// A credential dictated in two pieces to slip past filters: "the token
|
|
94
128
|
// starts with ghp_ and continues with <body>". Needs a credential noun,
|
|
@@ -133,9 +167,11 @@ function findSecrets(text) {
|
|
|
133
167
|
re.lastIndex = 0;
|
|
134
168
|
let m;
|
|
135
169
|
while ((m = re.exec(text)) !== null) {
|
|
136
|
-
// For the labelled
|
|
137
|
-
|
|
138
|
-
const
|
|
170
|
+
// For the labelled patterns, redact only the value, not the label. A
|
|
171
|
+
// pattern with alternatives captures the value in whichever group matched.
|
|
172
|
+
const group = m.slice(1).find(g => g !== undefined);
|
|
173
|
+
const value = group !== undefined ? group : m[0];
|
|
174
|
+
const at = group !== undefined ? m.index + m[0].lastIndexOf(group) : m.index;
|
|
139
175
|
hits.push({ kind, index: at, length: value.length });
|
|
140
176
|
if (m[0].length === 0)
|
|
141
177
|
re.lastIndex++;
|
package/dist/engine/shelf.js
CHANGED
|
@@ -103,19 +103,39 @@ const VERSIONED = [
|
|
|
103
103
|
];
|
|
104
104
|
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
105
105
|
const VERSIONED_RE = new RegExp(`(?:^|[^a-z0-9.])(?:${VERSIONED.map(escape).join('|')})[\\s-]?v?\\d+(?:\\.\\d+)*(?![\\d.]*\\d)`, 'i');
|
|
106
|
+
/**
|
|
107
|
+
* X.Y.Z as a version, but not a thousands-separated number: in Spanish
|
|
108
|
+
* "1.200.000 visitas" is a count, and it made the fact volatile (2.11).
|
|
109
|
+
* Every group after the first exactly three digits = a number, not a version.
|
|
110
|
+
*/
|
|
111
|
+
function semverLike(t) {
|
|
112
|
+
const re = /(?:^|[^\d.])(\d{1,3}(?:\.\d{1,3}){2,})(?:-[0-9a-z.]+)?(?![\d.]*\d)/g;
|
|
113
|
+
for (const m of t.matchAll(re)) {
|
|
114
|
+
const grupos = m[1].split('.');
|
|
115
|
+
if (grupos.length !== 3)
|
|
116
|
+
continue;
|
|
117
|
+
if (grupos.slice(1).every(g => g.length === 3))
|
|
118
|
+
continue; // 1.200.000
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
106
123
|
const RULES = [
|
|
107
124
|
{
|
|
108
125
|
reason: 'version',
|
|
109
126
|
test: (_raw, t) => /\bv\d+(?:\.\d+)+\b/.test(t) // v2.3
|
|
110
127
|
|| /\b(?:version|release)\s*:?\s*v?\d+(?:\.\d+)*/.test(t) // version 3.6, versión 5
|
|
111
|
-
||
|
|
128
|
+
|| semverLike(t) // 2.8.0, 1.4.2-beta (not a dd.mm.yyyy day, not 1.200.000)
|
|
112
129
|
|| VERSIONED_RE.test(t), // PostgreSQL 14, Node 22
|
|
113
130
|
},
|
|
114
131
|
{
|
|
115
132
|
reason: 'price',
|
|
116
133
|
test: (_raw, t) => /[€$£¥]\s?\d|\d(?:[.,]\d+)?\s?[€$£¥]/.test(t) // $49, 29 €
|
|
117
134
|
|| /\b\d+(?:[.,]\d+)?\s*(?:euros?|eur|usd|dollars?|dolares|libras|pounds?|gbp|mxn|pesos)\b/.test(t)
|
|
118
|
-
|
|
135
|
+
// A number right before the period ("29 al mes", "29/mes", "9.99 per month"). Not
|
|
136
|
+
// "3 informes mensuales" or "4 informes al mes" (2.11): a count of things is no
|
|
137
|
+
// price, and with money in the sentence the currency rules above already fire.
|
|
138
|
+
|| /\d(?:[.,]\d+)?\s?(?:k\s?)?(?:al mes\b|por mes\b|\/mes\b|per month\b|a month\b|\/month\b|\/mo\b|al ano\b|por ano\b|per year\b|\/year\b|\/yr\b)/.test(t)
|
|
119
139
|
|| /\b(?:price|pricing|precio|precios|tarifa|tarifas|cuesta|cuestan|cost|costs|fee|cuota)\b[^.\n]{0,30}\d/.test(t),
|
|
120
140
|
},
|
|
121
141
|
{
|
|
@@ -145,7 +165,9 @@ const RULES = [
|
|
|
145
165
|
test: (raw, t) => /\b[A-Z][A-Z0-9]*_[A-Z0-9_]+\s*=\s*\S+/.test(raw) // KEY_NAME=value
|
|
146
166
|
|| /(?:^|\s)--[a-z][\w-]*=\S+/.test(raw) // --max-old-space-size=4096
|
|
147
167
|
|| /\b[a-z][a-z0-9]*_[a-z0-9_]+\b\s*(?:=|:|de|a|en|to|at|is|es)?\s*\d/.test(t) // memory_limit de 512M
|
|
148
|
-
|| /\b(?:set to|configured (?:to|as|at)|configurad[oa]s? (?:a|en|como)|fijad[oa]s? (?:a|en)|establecid[oa]s? (?:a|en)|timeout|time-out|ttl|rate limit|limit|limite|
|
|
168
|
+
|| /\b(?:set to|configured (?:to|as|at)|configurad[oa]s? (?:a|en|como)|fijad[oa]s? (?:a|en)|establecid[oa]s? (?:a|en)|timeout|time-out|ttl|rate limit|limit|limite|maximo|maximum|minimo|minimum|retries|reintentos|threshold|umbral|batch size|pool size|flag)\b[^.\n]{0,25}?\d/.test(t)
|
|
169
|
+
// "max" only as written in lower case: "Max Planck nacio en 1858" is a name (2.11).
|
|
170
|
+
|| /\b(?:max|MAX)\b[^.\n]{0,25}?\d/.test(raw),
|
|
149
171
|
},
|
|
150
172
|
];
|
|
151
173
|
/** Role words, as written (first letter either case, rest lower) or as acronyms. */
|