engineering-memory 1.11.0 → 1.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engineering-memory",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
4
4
  "description": "Installs the Engineering Memory skill and its local MCP bridge. Sign in after installing; your organization and project are resolved from your account.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1019,16 +1019,26 @@ export class BridgeService {
1019
1019
  if (!run || typeof run.id !== 'string')
1020
1020
  throw refuse('The inspection response has no run identity.', 'memory.sync_status');
1021
1021
  if (run.state === 'inventory' || run.state === 'planning') {
1022
- for (let offset = 0; offset < snapshot.files.length; offset += 1000) {
1023
- const page = cleanJson({
1022
+ for (let offset = 0; offset < snapshot.files.length;) {
1023
+ const page = {
1024
1024
  projectId: input.projectId,
1025
1025
  runId: run.id,
1026
- files: snapshot.files.slice(offset, offset + 1000),
1027
- });
1028
- assertSafeToPersist(page);
1026
+ files: [],
1027
+ };
1028
+ let bytes = Buffer.byteLength(JSON.stringify(page));
1029
+ while (offset < snapshot.files.length && page.files.length < 1000) {
1030
+ const file = snapshot.files[offset];
1031
+ const addedBytes = Buffer.byteLength(JSON.stringify(file)) + (page.files.length ? 1 : 0);
1032
+ if (page.files.length && bytes + addedBytes > 64 * 1024)
1033
+ break;
1034
+ page.files.push(file);
1035
+ bytes += addedBytes;
1036
+ offset += 1;
1037
+ }
1038
+ assertSafeToPersist(cleanJson(page));
1029
1039
  await this.dependencies.client.request('/memory/sync/inventory', {
1030
1040
  method: 'POST',
1031
- body: page,
1041
+ body: cleanJson(page),
1032
1042
  });
1033
1043
  }
1034
1044
  await this.dependencies.client.request('/memory/sync/inventory', {
@@ -4,14 +4,139 @@ import { join } from 'node:path';
4
4
  import { assertManagedPath, ensureManagedDirectory, readJson, removeFile, writeJson, } from '../utilities/files.js';
5
5
  import { sha256, stableStringify } from '../utilities/hash.js';
6
6
  const sensitiveKeyPattern = /(authorization|password|passcode|secret|token|cookie|email|phone|mobile|customer|account|user.?id|user.?name)/i;
7
- const emailPattern = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
7
+ const emailSource = '(?:^|[^\\p{L}\\p{N}._%+-])[\\p{L}\\p{N}._%+-]{1,64}@([\\p{L}\\p{N}.-]+\\.[\\p{L}]{2,63})(?=$|[^\\p{L}\\p{N}.-])';
8
+ const emailPattern = new RegExp(emailSource, 'iu');
9
+ const emailMatchPattern = new RegExp(emailSource, 'giu');
8
10
  const phonePattern = /\+\d[\d ()-]{7,}\d/;
11
+ const repositoryPathFields = new Set([
12
+ 'path',
13
+ 'paths',
14
+ 'originalPath',
15
+ 'changedPaths',
16
+ 'pathPatterns',
17
+ 'evidencePaths',
18
+ 'screenPathPatterns',
19
+ 'componentPathPatterns',
20
+ 'pathChanges',
21
+ 'templatePath',
22
+ ]);
23
+ const topLevelDomains = new Set([
24
+ 'com',
25
+ 'net',
26
+ 'org',
27
+ 'edu',
28
+ 'gov',
29
+ 'mil',
30
+ 'int',
31
+ 'info',
32
+ 'biz',
33
+ 'name',
34
+ 'pro',
35
+ 'mobi',
36
+ 'tel',
37
+ 'asia',
38
+ 'jobs',
39
+ 'coop',
40
+ 'aero',
41
+ 'museum',
42
+ 'travel',
43
+ 'app',
44
+ 'dev',
45
+ 'cloud',
46
+ 'online',
47
+ 'site',
48
+ 'store',
49
+ 'tech',
50
+ 'space',
51
+ 'website',
52
+ 'digital',
53
+ 'agency',
54
+ 'systems',
55
+ 'solutions',
56
+ 'email',
57
+ 'xyz',
58
+ 'top',
59
+ 'club',
60
+ 'live',
61
+ 'life',
62
+ 'world',
63
+ 'today',
64
+ 'network',
65
+ 'group',
66
+ 'company',
67
+ 'center',
68
+ 'media',
69
+ 'studio',
70
+ 'design',
71
+ 'software',
72
+ 'tools',
73
+ 'works',
74
+ 'team',
75
+ 'zone',
76
+ 'one',
77
+ 'ltd',
78
+ 'inc',
79
+ 'llc',
80
+ 'gmbh',
81
+ 'shop',
82
+ 'page',
83
+ 'blog',
84
+ 'wiki',
85
+ 'news',
86
+ 'art',
87
+ 'link',
88
+ 'host',
89
+ 'press',
90
+ 'fyi',
91
+ 'test',
92
+ 'example',
93
+ 'invalid',
94
+ 'local',
95
+ 'localhost',
96
+ 'internal',
97
+ ]);
98
+ const countryCodeTopLevelDomains = new Set('ac ad ae af ag ai al am ao aq ar as at au aw ax az ba bb bd be bf bg bh bi bj bm bn bo br bs bt bw by bz ca cc cd cf cg ch ci ck cl cm cn co cr cu cv cw cx cy cz de dj dk dm do dz ec ee eg er es et eu fi fj fk fm fo fr ga gd ge gf gg gh gi gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in io iq ir is it je jm jo jp ke kg kh ki km kn kp kr kw ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mg mh mk ml mm mn mo mp mq mr ms mt mu mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl pm pn pr ps pt pw py qa re ro rs ru rw sa sb sc sd se sg sh si sk sl sm sn so sr ss st su sv sx sy sz tc td tf tg th tj tk tl tm tn to tr tt tv tw tz ua ug uk us uy uz va vc ve vg vi vn vu wf ws ye yt za zm zw'.split(' '));
99
+ function isTopLevelDomain(label) {
100
+ const value = label.toLowerCase();
101
+ return countryCodeTopLevelDomains.has(value) || topLevelDomains.has(value);
102
+ }
103
+ const extensionTopLevelDomains = new Set([
104
+ 'ai',
105
+ 'app',
106
+ 'as',
107
+ 'cc',
108
+ 'md',
109
+ 'ml',
110
+ 'mm',
111
+ 'pl',
112
+ 'py',
113
+ 'rs',
114
+ 'sh',
115
+ 'so',
116
+ ]);
117
+ function hasEmailAddress(value, repositoryPath) {
118
+ if (!repositoryPath)
119
+ return emailPattern.test(value);
120
+ for (const match of value.matchAll(emailMatchPattern)) {
121
+ const labels = (match[1] ?? '').split('.');
122
+ const last = (labels[labels.length - 1] ?? '').toLowerCase();
123
+ if (isTopLevelDomain(last) && !extensionTopLevelDomains.has(last))
124
+ return true;
125
+ const beforeExtension = (labels[labels.length - 2] ?? '').toLowerCase();
126
+ if (labels.length >= 3 &&
127
+ topLevelDomains.has(beforeExtension) &&
128
+ !extensionTopLevelDomains.has(beforeExtension))
129
+ return true;
130
+ }
131
+ return false;
132
+ }
133
+ const knownCredentialPattern = /\b(?:A(?:KI|SI)A[0-9A-Z]{16}|gh[pousr]_[A-Z0-9]{30,}|github_pat_[A-Z0-9_]{30,}|sk_live_[A-Z0-9]{16,}|rk_live_[A-Z0-9]{16,}|xox[baprs]-[A-Z0-9-]{12,}|AIza[A-Z0-9_-]{35}|sk-(?:proj-)?[A-Z0-9_-]{20,})\b/i;
9
134
  const bearerPattern = /\bbearer\s+[a-z0-9._~-]+/i;
10
135
  const jwtPattern = /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/;
11
136
  const privateKeyPattern = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/;
12
137
  const credentialUrlPattern = /\b[a-z][a-z0-9+.-]*:\/\/[^\s/@:]+:[^\s/@]+@/i;
13
138
  const ipv4Pattern = /\b(?:\d{1,3}\.){3}\d{1,3}\b/;
14
- const personalHomePathPattern = /(?:^|[\s"'(])(?:[A-Z]:[\\/]+Users[\\/]+[^\\/\s"']+(?:[\\/]|(?=$|\s))|\/(?:Users|home)\/[^/\s"']+(?:\/|(?=$|\s)))/i;
139
+ const personalHomePathPattern = /(?:[A-Z]:[\\/](?:Users|Documents and Settings)[\\/][^\\/\s"'<>:|?*]+|(?:^|[\s"'(<=])\/(?:Users|home)\/[^/\s"'<>*]+)(?:[\\/]|$)/i;
15
140
  export class OfflineOutbox {
16
141
  root;
17
142
  queues = new Map();
@@ -155,15 +280,17 @@ export function assertSafeToPersist(value, key = '') {
155
280
  throw new Error(`Sensitive field cannot be persisted: ${key}`);
156
281
  }
157
282
  if (typeof value === 'string') {
158
- if (emailPattern.test(value) ||
283
+ const repositoryPath = repositoryPathFields.has(key);
284
+ if (hasEmailAddress(value, repositoryPath) ||
159
285
  phonePattern.test(value) ||
160
286
  bearerPattern.test(value) ||
161
287
  jwtPattern.test(value) ||
162
288
  privateKeyPattern.test(value) ||
289
+ knownCredentialPattern.test(value) ||
163
290
  credentialUrlPattern.test(value) ||
164
- ipv4Pattern.test(value) ||
165
- personalHomePathPattern.test(value)) {
166
- throw new Error('PII or credentials cannot be persisted');
291
+ personalHomePathPattern.test(value) ||
292
+ (!repositoryPath && ipv4Pattern.test(value))) {
293
+ throw new Error(`PII or credentials cannot be persisted: ${key} ${value}`);
167
294
  }
168
295
  return;
169
296
  }
@@ -199,13 +326,13 @@ export function redactForCache(value) {
199
326
  return null;
200
327
  }
201
328
  }
202
- function redactValue(value) {
329
+ function redactValue(value, key = '') {
203
330
  if (typeof value === 'string') {
204
- assertSafeToPersist(value);
331
+ assertSafeToPersist(value, key);
205
332
  return value;
206
333
  }
207
334
  if (Array.isArray(value)) {
208
- return value.map((entry) => redactValue(entry));
335
+ return value.map((entry) => redactValue(entry, key));
209
336
  }
210
337
  if (value !== null && typeof value === 'object') {
211
338
  const result = {};
@@ -213,7 +340,7 @@ function redactValue(value) {
213
340
  if (sensitiveKeyPattern.test(key)) {
214
341
  continue;
215
342
  }
216
- result[key] = redactValue(entry);
343
+ result[key] = redactValue(entry, key);
217
344
  }
218
345
  return result;
219
346
  }