dsh-prime-memory 0.18.2 → 0.18.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.
Files changed (46) hide show
  1. package/CHANGELOG.en.md +14 -0
  2. package/CHANGELOG.ja.md +14 -0
  3. package/CHANGELOG.ko.md +14 -0
  4. package/CHANGELOG.md +14 -0
  5. package/README.en.md +4 -0
  6. package/README.ja.md +4 -0
  7. package/README.ko.md +4 -0
  8. package/README.md +4 -0
  9. package/cordis.patch.yml +6 -0
  10. package/dist/client.js +36 -9
  11. package/dist/config.d.ts +37 -0
  12. package/dist/config.js +7 -0
  13. package/dist/contract.d.ts +46 -0
  14. package/dist/hooks/capture-recovery.d.ts +59 -0
  15. package/dist/hooks/capture-recovery.js +88 -0
  16. package/dist/hooks/capture.js +110 -1
  17. package/dist/hooks/recall-ack.d.ts +48 -0
  18. package/dist/hooks/recall-ack.js +79 -0
  19. package/dist/hooks/recall.d.ts +19 -0
  20. package/dist/hooks/recall.js +114 -5
  21. package/dist/index.d.ts +22 -0
  22. package/dist/index.js +4 -0
  23. package/dist/pipeline/l1.d.ts +4 -0
  24. package/dist/pipeline/l1.js +9 -2
  25. package/dist/pipeline/reconcile.js +4 -0
  26. package/dist/pipeline/runner.js +62 -0
  27. package/dist/prompts/boundary.d.ts +17 -0
  28. package/dist/prompts/boundary.js +20 -0
  29. package/dist/prompts/graph-projection.js +4 -1
  30. package/dist/prompts/l1-dedup.d.ts +3 -3
  31. package/dist/prompts/l1-dedup.js +9 -1
  32. package/dist/prompts/l1-extraction.d.ts +3 -3
  33. package/dist/prompts/l1-extraction.js +12 -0
  34. package/dist/prompts/persona.js +6 -0
  35. package/dist/prompts/scene.js +7 -1
  36. package/dist/stats.js +9 -0
  37. package/dist/store/l0.d.ts +4 -0
  38. package/dist/store/l0.js +8 -0
  39. package/dist/store/sqlite.d.ts +12 -0
  40. package/dist/store/sqlite.js +37 -0
  41. package/dist/store/trace.d.ts +39 -0
  42. package/dist/store/trace.js +149 -0
  43. package/dist/tools/index.js +25 -1
  44. package/dist/util/redact.d.ts +27 -0
  45. package/dist/util/redact.js +228 -0
  46. package/package.json +1 -1
@@ -0,0 +1,228 @@
1
+ /**
2
+ * §C 载荷脱敏(memorax-absorb Wave 2 / task_14)——捕获边界的密钥擦除。
3
+ *
4
+ * 用户在对话里贴入的密钥(PEM/Bearer/JWT/Cookie/厂商 API key/邮箱/长号/高熵 ID)
5
+ * 若原样进 L0/L1,会被逐轮召回注入上下文并在 UI 展示。本模块在捕获单点把它们
6
+ * 替换为类型化占位符 `[REDACTED:<KIND>]`——占位符保留类别语义,关键词检索仍可命中。
7
+ *
8
+ * 词表/正则/allowlist/span 合并算法以 memorax `payload-redaction.ts` 为基准逐条移植
9
+ * (全文快照见计划 evidence/payload-redaction-snapshot.ts;findings.md 有等价内联)。
10
+ *
11
+ * 契约(sanitize 同款,tests/redact.test.ts 钉住):
12
+ * - 纯函数,零 I/O 零时钟零随机;输入原样返回(未命中)= 引用不变无关紧要,字符串不可变;
13
+ * - **幂等**:已是 `[REDACTED:KIND]` 的值不会被二次匹配;
14
+ * - 安全值 allowlist 命中不脱(`example`/`change-me`/`$VAR`/`${{…}}` 等);
15
+ * - 本函数**永不抛错**的保证由调用方 try/catch 兜底(capture 接线层,§C 红线:失败放行原文)。
16
+ */
17
+ const PLACEHOLDER_VALUE_PATTERN = /^\[REDACTED:[A-Z_]+\]$/;
18
+ const SENSITIVE_KEY_SOURCE = String.raw `(?:[A-Za-z_$][A-Za-z0-9_$-]*)?(?:password|passwd|pwd|client[-_$]?secret|consumer[-_$]?secret|api[-_$]?key|access[-_$]?token|refresh[-_$]?token|auth[-_$]?token|id[-_$]?token|private[-_$]?key|secret[-_$]?access[-_$]?key|secret[-_$]?key|credential|secret|token)`;
19
+ const CREDENTIAL_VALUE_SOURCE = String.raw `(?:\[REDACTED:[A-Z_]+\]|\$\{\{[^\r\n]*?\}\}|\{\{[^\r\n]*?\}\}|\$\{[^}\r\n]+\}|\$[A-Za-z_][A-Za-z0-9_]*|"(?:\\.|[^"\\\r\n])*"|'(?:\\.|[^'\\\r\n])*'|[^,;#}\]\)\r\n]+)`;
20
+ /** 安全值 allowlist:命中即放行(示例/占位/环境变量引用/类型名等,脱了只会毁记忆)。 */
21
+ const SAFE_CREDENTIAL_VALUE_PATTERN = /^(?:\[REDACTED:[A-Z_]+\]|\$\{\{[^\r\n]*\}\}|\{\{[^\r\n]*\}\}|\$\{[A-Z_][A-Z0-9_]*\}|\$[A-Z_][A-Z0-9_]*|(?:process\.)?env\.[A-Z_][A-Z0-9_]*|<[^>]+>|--\S+|x{2,}|\*{2,}|\.{3}|sk[_-]x+|example|sample|change-?me|(?:your|replace)[-_][a-z0-9_-]+|string|str|number|boolean|unknown|any|null|undefined|none)$/i;
22
+ const RULES = [
23
+ {
24
+ // PEM 私钥块:至对应 END,截断文件时取到文末
25
+ pattern: /-----BEGIN ((?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED) )?PRIVATE KEY|PGP PRIVATE KEY BLOCK)-----[\s\S]*?(?:-----END \1-----|$)/gi,
26
+ kind: 'PRIVATE_KEY',
27
+ priority: 100,
28
+ },
29
+ {
30
+ pattern: /(?:^|[\r\n])[ \t]*(?:Proxy-)?Authorization[ \t]*:[ \t]*([^ \t\r\n][^\r\n]*)/gim,
31
+ group: 1,
32
+ kind: 'AUTH_TOKEN',
33
+ priority: 95,
34
+ },
35
+ {
36
+ pattern: /(?:^|[^A-Za-z0-9_-])(?:Bearer|Basic)\s+["']?([A-Za-z0-9._~+/=-]{16,})(?![A-Za-z0-9._~+/=-])/gi,
37
+ group: 1,
38
+ kind: 'AUTH_TOKEN',
39
+ priority: 94,
40
+ },
41
+ {
42
+ // JWT 三段形态
43
+ pattern: /\beyJ[A-Za-z0-9_-]{5,}\.eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{16,}\b/g,
44
+ kind: 'AUTH_TOKEN',
45
+ priority: 94,
46
+ },
47
+ {
48
+ pattern: /(?:^|[\r\n])[ \t]*(?:Set-Cookie|Cookie)\s*:\s*([^\r\n]+)/gim,
49
+ group: 1,
50
+ kind: 'COOKIE',
51
+ priority: 92,
52
+ },
53
+ {
54
+ // 声明式赋值:key: value / key = value(含 export/const/let/var 前缀与引号键)
55
+ pattern: new RegExp(String.raw `(?:^|[\r\n;,{}])[ \t]*(?:-[ \t]+)?(?:(?:export|const|let|var)[ \t]+)*["']?${SENSITIVE_KEY_SOURCE}["']?[ \t]*[:=][ \t]*(${CREDENTIAL_VALUE_SOURCE})`, 'gim'),
56
+ group: 1,
57
+ kind: 'CREDENTIAL',
58
+ priority: 85,
59
+ accept: (value) => !isSafeCredentialValue(value),
60
+ },
61
+ {
62
+ // CLI 形态:--api-key value / --api-key=value
63
+ pattern: new RegExp(String.raw `--${SENSITIVE_KEY_SOURCE}(?![A-Za-z0-9_$-])(?:[ \t]+|=[ \t]*)(${CREDENTIAL_VALUE_SOURCE})`, 'gi'),
64
+ group: 1,
65
+ kind: 'CREDENTIAL',
66
+ priority: 85,
67
+ accept: (value) => !isSafeCredentialValue(value),
68
+ },
69
+ {
70
+ // URL query 形态:?api_key=...
71
+ pattern: new RegExp(String.raw `[?&]${SENSITIVE_KEY_SOURCE}(?![A-Za-z0-9_$-])=([^&#\s"'<>]+)`, 'gi'),
72
+ group: 1,
73
+ kind: 'CREDENTIAL',
74
+ priority: 85,
75
+ accept: (value) => !isSafeCredentialValue(value),
76
+ },
77
+ {
78
+ // URL userinfo 形态:scheme://user:pass@
79
+ pattern: /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s/:@]*:([^\s/@]+)@/g,
80
+ group: 1,
81
+ kind: 'CREDENTIAL',
82
+ priority: 85,
83
+ accept: (value) => !isSafeCredentialValue(value),
84
+ },
85
+ // ── 已知厂商 API key 形态(9 条) ──
86
+ { pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{16,}[A-Za-z0-9]\b/g, kind: 'API_KEY', priority: 80 },
87
+ { pattern: /\bsk_[A-Za-z0-9_-]{8,}\b/g, kind: 'API_KEY', priority: 80 },
88
+ { pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g, kind: 'API_KEY', priority: 80 },
89
+ { pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, kind: 'API_KEY', priority: 80 },
90
+ { pattern: /\bxox[baprs]-[A-Za-z0-9-]{16,}[A-Za-z0-9]\b/g, kind: 'API_KEY', priority: 80 },
91
+ { pattern: /\b(?:(?:sk|rk)_live_[A-Za-z0-9]{16,}|whsec_[A-Za-z0-9]{16,})\b/g, kind: 'API_KEY', priority: 80 },
92
+ { pattern: /\bglpat-[A-Za-z0-9_-]{16,}[A-Za-z0-9]\b/g, kind: 'API_KEY', priority: 80 },
93
+ { pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g, kind: 'API_KEY', priority: 80 },
94
+ { pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, kind: 'API_KEY', priority: 80 },
95
+ {
96
+ // 邮箱(前后向断言防误切)
97
+ pattern: /(?<![A-Z0-9._%+-])[A-Z0-9._%+-]{1,64}@[A-Z0-9](?:[A-Z0-9.-]{0,251}[A-Z0-9])?\.[A-Z]{2,63}(?![A-Z0-9-]|\.[A-Z0-9])/gi,
98
+ kind: 'EMAIL',
99
+ priority: 70,
100
+ },
101
+ {
102
+ // UUID
103
+ pattern: /(?<![A-Za-z0-9])[0-9A-Fa-f]{8}-(?:[0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}(?![A-Za-z0-9])/g,
104
+ kind: 'OPAQUE_ID',
105
+ priority: 60,
106
+ },
107
+ {
108
+ // hex 64/40/32(含字母才脱——纯数字是普通长号,交 LONG_NUMBER)
109
+ pattern: /(?<![A-Za-z0-9])(?:[0-9A-Fa-f]{64}|[0-9A-Fa-f]{40}|[0-9A-Fa-f]{32})(?![A-Za-z0-9])/g,
110
+ kind: 'OPAQUE_ID',
111
+ priority: 60,
112
+ accept: (value) => /[A-Fa-f]/.test(value),
113
+ },
114
+ {
115
+ // ≥24 位字母数字高熵启发式(熵参数见 looksLikeHighEntropyOpaqueId)
116
+ pattern: /(?<![A-Za-z0-9])[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g,
117
+ kind: 'OPAQUE_ID',
118
+ priority: 60,
119
+ accept: looksLikeHighEntropyOpaqueId,
120
+ },
121
+ {
122
+ // ≥9 位数字串(含电话形);日期形放行
123
+ pattern: /(?<![A-Za-z0-9.])\+?(?:\d|\(\d)[\d ()-]*\d[Xx]?\)?(?![A-Za-z0-9]|\.\d)/g,
124
+ kind: 'LONG_NUMBER',
125
+ priority: 50,
126
+ accept: (value) => {
127
+ if (value.replace(/\D/g, '').length < 9)
128
+ return false;
129
+ const normalized = value.trim().replace(/[()]/g, '');
130
+ return (!/^(?:19|20)\d{2}-\d{1,2}-\d{1,2}(?:[ T-]\d{1,2})?$/.test(normalized) &&
131
+ !/^(?:19|20)\d{2}\s+\d{1,2}\s+\d{1,2}(?:\s+\d{1,2})?$/.test(normalized));
132
+ },
133
+ },
134
+ ];
135
+ /**
136
+ * 对文本执行 8 类脱敏。规则各自扫描 → 收集值级 span → 合并重叠 → 重拼插入占位符。
137
+ * 同一位置被多条规则命中时取更长/更高优先级的 span。
138
+ */
139
+ export function redactSecrets(text) {
140
+ if (!text)
141
+ return { text, counts: {}, redacted: false };
142
+ const spans = [];
143
+ for (const rule of RULES)
144
+ collectRuleSpans(text, rule, spans);
145
+ const selected = mergeOverlappingSpans(spans);
146
+ if (selected.length === 0)
147
+ return { text, counts: {}, redacted: false };
148
+ const counts = {};
149
+ const output = [];
150
+ let offset = 0;
151
+ for (const span of selected) {
152
+ output.push(text.slice(offset, span.start), `[REDACTED:${span.kind}]`);
153
+ counts[span.kind] = (counts[span.kind] ?? 0) + 1;
154
+ offset = span.end;
155
+ }
156
+ output.push(text.slice(offset));
157
+ return { text: output.join(''), counts, redacted: true };
158
+ }
159
+ function collectRuleSpans(text, rule, spans) {
160
+ rule.pattern.lastIndex = 0;
161
+ for (const match of text.matchAll(rule.pattern)) {
162
+ const value = match[rule.group ?? 0];
163
+ if (match.index === undefined || !value || PLACEHOLDER_VALUE_PATTERN.test(value.trim()))
164
+ continue;
165
+ if (rule.accept && !rule.accept(value))
166
+ continue;
167
+ const relativeStart = match[0].lastIndexOf(value);
168
+ if (relativeStart < 0)
169
+ continue;
170
+ spans.push({
171
+ start: match.index + relativeStart,
172
+ end: match.index + relativeStart + value.length,
173
+ kind: rule.kind,
174
+ priority: rule.priority,
175
+ });
176
+ }
177
+ }
178
+ function isSafeCredentialValue(rawValue) {
179
+ let value = rawValue.trim();
180
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
181
+ value = value.slice(1, -1).trim();
182
+ }
183
+ return !value || SAFE_CREDENTIAL_VALUE_PATTERN.test(value);
184
+ }
185
+ /** 高熵判定:小写≥2 ∧ 大写≥2 ∧ 数字≥2 ∧ 字符类切换≥4 ∧ 最长连续字母段≤5 ∧ 去重字符≥12。 */
186
+ function looksLikeHighEntropyOpaqueId(value) {
187
+ let lowercase = 0;
188
+ let uppercase = 0;
189
+ let digits = 0;
190
+ let transitions = 0;
191
+ let letterRun = 0;
192
+ let longestLetterRun = 0;
193
+ let previousWasDigit;
194
+ const unique = new Set();
195
+ for (const character of value) {
196
+ const isDigit = character >= '0' && character <= '9';
197
+ if (isDigit) {
198
+ digits += 1;
199
+ letterRun = 0;
200
+ }
201
+ else {
202
+ if (character >= 'a' && character <= 'z')
203
+ lowercase += 1;
204
+ else
205
+ uppercase += 1;
206
+ letterRun += 1;
207
+ longestLetterRun = Math.max(longestLetterRun, letterRun);
208
+ }
209
+ if (previousWasDigit !== undefined && previousWasDigit !== isDigit)
210
+ transitions += 1;
211
+ previousWasDigit = isDigit;
212
+ unique.add(character);
213
+ }
214
+ return lowercase >= 2 && uppercase >= 2 && digits >= 2 && transitions >= 4 && longestLetterRun <= 5 && unique.size >= 12;
215
+ }
216
+ function mergeOverlappingSpans(spans) {
217
+ const sorted = [...spans].sort((left, right) => left.start - right.start || right.end - left.end || right.priority - left.priority);
218
+ const merged = [];
219
+ for (const span of sorted) {
220
+ const previous = merged.at(-1);
221
+ if (!previous || span.start >= previous.end) {
222
+ merged.push({ ...span });
223
+ continue;
224
+ }
225
+ previous.end = Math.max(previous.end, span.end);
226
+ }
227
+ return merged;
228
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-prime-memory",
3
- "version": "0.18.2",
3
+ "version": "0.18.4",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public",