crbro-memory 2.9.1 → 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 -456
- package/SECURITY.md +135 -8
- package/bin/crbro.mjs +555 -299
- 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 +143 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +762 -311
- package/dist/engine/doctor.js +676 -0
- package/dist/engine/evidence.js +171 -0
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +50 -36
- 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 +735 -144
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +126 -69
- package/dist/engine/projects.js +80 -0
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +34 -11
- 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 +810 -176
- package/dist/search/quality.js +31 -0
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +649 -343
- package/dist/sync/git.js +203 -72
- package/dist/sync/materialize.js +33 -8
- package/dist/sync/space.js +286 -101
- 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 +219 -52
- 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 +12 -8
- 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 -670
- 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/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 -61
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.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 -156
- 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 -323
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.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 -35
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -142
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -116
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -346
- 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 -11
- 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
|
@@ -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. */
|
|
@@ -452,11 +474,11 @@ function recordVerdict(text) {
|
|
|
452
474
|
* record of something done (2.9.1), else volatile when a rule fires (and
|
|
453
475
|
* which one), normal otherwise.
|
|
454
476
|
*/
|
|
455
|
-
function detectShelf(text) {
|
|
477
|
+
function detectShelf(text, temporal) {
|
|
456
478
|
const raw = String(text || '');
|
|
457
479
|
if (!raw.trim())
|
|
458
480
|
return { shelf: 'normal' };
|
|
459
|
-
if (isDatedRecord(raw))
|
|
481
|
+
if (temporal !== 'current' && isDatedRecord(raw))
|
|
460
482
|
return { shelf: 'permanent', reason: 'history' };
|
|
461
483
|
const t = fold(raw);
|
|
462
484
|
for (const r of RULES) {
|
|
@@ -474,17 +496,18 @@ function detectShelf(text) {
|
|
|
474
496
|
* made about the present — warning on it is noise); else the one its text
|
|
475
497
|
* implies.
|
|
476
498
|
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
* by its text like any other claim about the present.
|
|
499
|
+
* `current` impide inferir historia por origen o texto, sin verificar nada.
|
|
500
|
+
* Sin temporalidad explícita se conserva la clasificación legacy por
|
|
501
|
+
* confirmations; repetir ya no cambia la fecha de verificación.
|
|
481
502
|
*/
|
|
482
503
|
function shelfOfFact(f) {
|
|
483
504
|
if (isShelfLife(f.shelf_life))
|
|
484
505
|
return { shelf: f.shelf_life, inferred: false };
|
|
485
|
-
if (f.
|
|
506
|
+
if (f.temporal === 'historical')
|
|
507
|
+
return { shelf: 'permanent', inferred: true, reason: 'history' };
|
|
508
|
+
if (f.temporal !== 'current' && f.source === 'miner' && !f.verified && (f.confirmations ?? 1) <= 1)
|
|
486
509
|
return { shelf: 'permanent', inferred: true, reason: 'miner' };
|
|
487
|
-
const d = detectShelf(f.text);
|
|
510
|
+
const d = detectShelf(f.text, f.temporal);
|
|
488
511
|
return { shelf: d.shelf, inferred: true, ...(d.reason ? { reason: d.reason } : {}) };
|
|
489
512
|
}
|
|
490
513
|
/**
|