not-manage 0.1.17
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 +176 -0
- package/NOTICE +5 -0
- package/README.md +227 -0
- package/bin/not-manage.js +9 -0
- package/package.json +55 -0
- package/src/cli.js +668 -0
- package/src/clio-api.js +384 -0
- package/src/commands-activities.js +356 -0
- package/src/commands-auth.js +465 -0
- package/src/commands-billable-clients.js +152 -0
- package/src/commands-billable-matters.js +150 -0
- package/src/commands-bills.js +250 -0
- package/src/commands-contacts.js +179 -0
- package/src/commands-matters.js +214 -0
- package/src/commands-practice-areas.js +249 -0
- package/src/commands-tasks.js +213 -0
- package/src/commands-users.js +192 -0
- package/src/constants.js +50 -0
- package/src/keychain.js +63 -0
- package/src/oauth-callback.js +107 -0
- package/src/open-browser.js +33 -0
- package/src/postinstall.js +140 -0
- package/src/prompt.js +103 -0
- package/src/redaction.js +568 -0
- package/src/redirect-uri.js +53 -0
- package/src/resource-utils.js +141 -0
- package/src/store.js +178 -0
package/src/redaction.js
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
const CLIENT_OBJECT_KEYS = new Set(["client", "clients", "contact", "contacts"]);
|
|
2
|
+
const CONTACT_LIKE_RESOURCE_TYPES = new Set(["contact", "billable-client"]);
|
|
3
|
+
const SAFE_IDENTITY_RESOURCE_TYPES = new Set(["user"]);
|
|
4
|
+
const FREE_TEXT_FIELDS = new Set(["description", "memo", "note", "reference", "subject"]);
|
|
5
|
+
const LABEL_FIELDS = new Set(["display_number", "number", "identifier", "title"]);
|
|
6
|
+
const MATTER_LABEL_FIELDS = new Set(["display_number", "number"]);
|
|
7
|
+
const NAME_FIELDS = new Set(["name", "first_name", "last_name"]);
|
|
8
|
+
const EMAIL_FIELDS = new Set([
|
|
9
|
+
"email",
|
|
10
|
+
"primary_email_address",
|
|
11
|
+
"secondary_email_address",
|
|
12
|
+
"clio_connect_email",
|
|
13
|
+
]);
|
|
14
|
+
const PHONE_FIELDS = new Set([
|
|
15
|
+
"phone_number",
|
|
16
|
+
"primary_phone_number",
|
|
17
|
+
"secondary_phone_number",
|
|
18
|
+
]);
|
|
19
|
+
const SAFE_IDENTITY_OBJECT_KEYS = new Set([
|
|
20
|
+
"user",
|
|
21
|
+
"assignee",
|
|
22
|
+
"assigner",
|
|
23
|
+
"responsible_attorney",
|
|
24
|
+
"responsible_staff",
|
|
25
|
+
"originating_attorney",
|
|
26
|
+
]);
|
|
27
|
+
const NAME_HEURISTIC_EXCLUDED_TOKENS = new Set([
|
|
28
|
+
"activity",
|
|
29
|
+
"area",
|
|
30
|
+
"attorney",
|
|
31
|
+
"australia",
|
|
32
|
+
"call",
|
|
33
|
+
"calendar",
|
|
34
|
+
"canada",
|
|
35
|
+
"case",
|
|
36
|
+
"client",
|
|
37
|
+
"complaint",
|
|
38
|
+
"complete",
|
|
39
|
+
"confirm",
|
|
40
|
+
"contact",
|
|
41
|
+
"date",
|
|
42
|
+
"demand",
|
|
43
|
+
"defendant",
|
|
44
|
+
"draft",
|
|
45
|
+
"email",
|
|
46
|
+
"employment",
|
|
47
|
+
"entry",
|
|
48
|
+
"europe",
|
|
49
|
+
"expense",
|
|
50
|
+
"family",
|
|
51
|
+
"file",
|
|
52
|
+
"financial",
|
|
53
|
+
"follow",
|
|
54
|
+
"injury",
|
|
55
|
+
"labor",
|
|
56
|
+
"law",
|
|
57
|
+
"limitations",
|
|
58
|
+
"matter",
|
|
59
|
+
"meet",
|
|
60
|
+
"monitor",
|
|
61
|
+
"motion",
|
|
62
|
+
"package",
|
|
63
|
+
"personal",
|
|
64
|
+
"plaintiff",
|
|
65
|
+
"prepare",
|
|
66
|
+
"practice",
|
|
67
|
+
"request",
|
|
68
|
+
"research",
|
|
69
|
+
"review",
|
|
70
|
+
"schedule",
|
|
71
|
+
"send",
|
|
72
|
+
"serve",
|
|
73
|
+
"settlement",
|
|
74
|
+
"spoke",
|
|
75
|
+
"states",
|
|
76
|
+
"talk",
|
|
77
|
+
"task",
|
|
78
|
+
"time",
|
|
79
|
+
"trust",
|
|
80
|
+
"united",
|
|
81
|
+
"upload",
|
|
82
|
+
"with",
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const PLACEHOLDERS = {
|
|
86
|
+
email: "[REDACTED_EMAIL]",
|
|
87
|
+
name: "[REDACTED_NAME]",
|
|
88
|
+
phone: "[REDACTED_PHONE]",
|
|
89
|
+
ssn: "[REDACTED_SSN]",
|
|
90
|
+
taxId: "[REDACTED_TAX_ID]",
|
|
91
|
+
};
|
|
92
|
+
const PERSON_NAME_SUFFIXES = new Set(["esq", "ii", "iii", "iv", "jr", "sr"]);
|
|
93
|
+
|
|
94
|
+
function escapeRegExp(value) {
|
|
95
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isObject(value) {
|
|
99
|
+
return value && typeof value === "object" && !Array.isArray(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isContactLikeResourceType(resourceType) {
|
|
103
|
+
return CONTACT_LIKE_RESOURCE_TYPES.has(resourceType);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isSafeIdentityResourceType(resourceType) {
|
|
107
|
+
return SAFE_IDENTITY_RESOURCE_TYPES.has(resourceType);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeString(value) {
|
|
111
|
+
return typeof value === "string" ? value.trim() : "";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function pushReplacement(replacements, dedupe, value, placeholder) {
|
|
115
|
+
const normalized = normalizeString(value);
|
|
116
|
+
if (!normalized) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const key = `${placeholder}:${normalized.toLowerCase()}`;
|
|
121
|
+
if (dedupe.has(key)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
dedupe.add(key);
|
|
126
|
+
replacements.push({
|
|
127
|
+
placeholder,
|
|
128
|
+
value: normalized,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function collectContactLikeReplacements(node, replacements, dedupe) {
|
|
133
|
+
if (!isObject(node)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pushReplacement(replacements, dedupe, node.name, PLACEHOLDERS.name);
|
|
138
|
+
|
|
139
|
+
const fullName = [node.first_name, node.last_name]
|
|
140
|
+
.map((value) => normalizeString(value))
|
|
141
|
+
.filter(Boolean)
|
|
142
|
+
.join(" ")
|
|
143
|
+
.trim();
|
|
144
|
+
pushReplacement(replacements, dedupe, fullName, PLACEHOLDERS.name);
|
|
145
|
+
|
|
146
|
+
EMAIL_FIELDS.forEach((field) => {
|
|
147
|
+
pushReplacement(replacements, dedupe, node[field], PLACEHOLDERS.email);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
PHONE_FIELDS.forEach((field) => {
|
|
151
|
+
pushReplacement(replacements, dedupe, node[field], PLACEHOLDERS.phone);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function collectSensitiveReplacements(
|
|
156
|
+
value,
|
|
157
|
+
resourceType,
|
|
158
|
+
contactLikeContext = isContactLikeResourceType(resourceType),
|
|
159
|
+
replacements = [],
|
|
160
|
+
dedupe = new Set()
|
|
161
|
+
) {
|
|
162
|
+
if (Array.isArray(value)) {
|
|
163
|
+
value.forEach((item) => {
|
|
164
|
+
collectSensitiveReplacements(item, resourceType, contactLikeContext, replacements, dedupe);
|
|
165
|
+
});
|
|
166
|
+
return replacements;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!isObject(value)) {
|
|
170
|
+
return replacements;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (contactLikeContext) {
|
|
174
|
+
collectContactLikeReplacements(value, replacements, dedupe);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
Object.entries(value).forEach(([key, child]) => {
|
|
178
|
+
const childContactLikeContext = contactLikeContext || CLIENT_OBJECT_KEYS.has(key);
|
|
179
|
+
collectSensitiveReplacements(
|
|
180
|
+
child,
|
|
181
|
+
resourceType,
|
|
182
|
+
childContactLikeContext,
|
|
183
|
+
replacements,
|
|
184
|
+
dedupe
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return replacements;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function pushPreservedName(preserved, value) {
|
|
192
|
+
const normalized = normalizeString(value);
|
|
193
|
+
if (!normalized) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
preserved.add(normalized.toLowerCase());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function collectSafeIdentityNames(
|
|
201
|
+
value,
|
|
202
|
+
safeIdentityContext = false,
|
|
203
|
+
preserved = new Set()
|
|
204
|
+
) {
|
|
205
|
+
if (Array.isArray(value)) {
|
|
206
|
+
value.forEach((item) => {
|
|
207
|
+
collectSafeIdentityNames(item, safeIdentityContext, preserved);
|
|
208
|
+
});
|
|
209
|
+
return preserved;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!isObject(value)) {
|
|
213
|
+
return preserved;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (safeIdentityContext) {
|
|
217
|
+
pushPreservedName(preserved, value.name);
|
|
218
|
+
|
|
219
|
+
const fullName = [value.first_name, value.last_name]
|
|
220
|
+
.map((item) => normalizeString(item))
|
|
221
|
+
.filter(Boolean)
|
|
222
|
+
.join(" ")
|
|
223
|
+
.trim();
|
|
224
|
+
pushPreservedName(preserved, fullName);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
Object.entries(value).forEach(([key, child]) => {
|
|
228
|
+
collectSafeIdentityNames(
|
|
229
|
+
child,
|
|
230
|
+
safeIdentityContext || SAFE_IDENTITY_OBJECT_KEYS.has(key),
|
|
231
|
+
preserved
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return preserved;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function derivePersonSurname(name) {
|
|
239
|
+
const tokens = normalizeString(name)
|
|
240
|
+
.split(/\s+/)
|
|
241
|
+
.map((token) => token.replace(/^[^A-Za-z]+|[^A-Za-z]+$/g, ""))
|
|
242
|
+
.filter(Boolean);
|
|
243
|
+
|
|
244
|
+
if (tokens.length < 2) {
|
|
245
|
+
return "";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let index = tokens.length - 1;
|
|
249
|
+
while (index > 0 && PERSON_NAME_SUFFIXES.has(tokens[index].toLowerCase())) {
|
|
250
|
+
index -= 1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return index > 0 ? tokens[index] : "";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function collectPersonClientLabelReplacements(
|
|
257
|
+
value,
|
|
258
|
+
clientContext = false,
|
|
259
|
+
replacements = [],
|
|
260
|
+
dedupe = new Set()
|
|
261
|
+
) {
|
|
262
|
+
if (Array.isArray(value)) {
|
|
263
|
+
value.forEach((item) => {
|
|
264
|
+
collectPersonClientLabelReplacements(item, clientContext, replacements, dedupe);
|
|
265
|
+
});
|
|
266
|
+
return replacements;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!isObject(value)) {
|
|
270
|
+
return replacements;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (clientContext && value.type === "Person") {
|
|
274
|
+
pushReplacement(
|
|
275
|
+
replacements,
|
|
276
|
+
dedupe,
|
|
277
|
+
derivePersonSurname(value.name),
|
|
278
|
+
PLACEHOLDERS.name
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
Object.entries(value).forEach(([key, child]) => {
|
|
283
|
+
collectPersonClientLabelReplacements(
|
|
284
|
+
child,
|
|
285
|
+
clientContext || CLIENT_OBJECT_KEYS.has(key),
|
|
286
|
+
replacements,
|
|
287
|
+
dedupe
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return replacements;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function replaceKnownSensitiveValues(text, replacements) {
|
|
295
|
+
return replacements
|
|
296
|
+
.slice()
|
|
297
|
+
.sort((left, right) => right.value.length - left.value.length)
|
|
298
|
+
.reduce((output, replacement) => {
|
|
299
|
+
const matcher = new RegExp(escapeRegExp(replacement.value), "gi");
|
|
300
|
+
return output.replace(matcher, replacement.placeholder);
|
|
301
|
+
}, text);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function redactPatternPii(text) {
|
|
305
|
+
let output = String(text);
|
|
306
|
+
output = output.replace(
|
|
307
|
+
/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
|
|
308
|
+
PLACEHOLDERS.email
|
|
309
|
+
);
|
|
310
|
+
output = output.replace(
|
|
311
|
+
/\b(?:\+?1[-.\s]*)?(?:\(\d{3}\)|\d{3})[-.\s]*\d{3}[-.\s]*\d{4}\b/g,
|
|
312
|
+
PLACEHOLDERS.phone
|
|
313
|
+
);
|
|
314
|
+
output = output.replace(/\b\d{3}-\d{2}-\d{4}\b/g, PLACEHOLDERS.ssn);
|
|
315
|
+
output = output.replace(/\b\d{2}-\d{7}\b/g, PLACEHOLDERS.taxId);
|
|
316
|
+
|
|
317
|
+
return output;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function isLikelyNameCandidate(tokens, preservedNames) {
|
|
321
|
+
const candidate = tokens.join(" ").trim();
|
|
322
|
+
if (!candidate) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (preservedNames.has(candidate.toLowerCase())) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return tokens.every((token) => {
|
|
331
|
+
const cleaned = token.replace(/^[^A-Za-z]+|[^A-Za-z]+$/g, "");
|
|
332
|
+
if (!/^[A-Z][a-z]+(?:[-'][A-Z][a-z]+)?$/.test(cleaned)) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
return !NAME_HEURISTIC_EXCLUDED_TOKENS.has(cleaned.toLowerCase());
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function redactLikelyBareNames(text, preservedNames) {
|
|
340
|
+
const tokens = Array.from(
|
|
341
|
+
String(text).matchAll(/\b[A-Z][a-z]+(?:[-'][A-Z][a-z]+)?\b/g),
|
|
342
|
+
(match) => ({
|
|
343
|
+
end: match.index + match[0].length,
|
|
344
|
+
start: match.index,
|
|
345
|
+
value: match[0],
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const spans = [];
|
|
350
|
+
|
|
351
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
352
|
+
let matched = false;
|
|
353
|
+
|
|
354
|
+
for (const size of [3, 2]) {
|
|
355
|
+
if (index + size > tokens.length) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const window = tokens.slice(index, index + size);
|
|
360
|
+
const joinedByWhitespace = window
|
|
361
|
+
.slice(1)
|
|
362
|
+
.every((token, offset) => /^\s+$/.test(text.slice(window[offset].end, token.start)));
|
|
363
|
+
|
|
364
|
+
if (!joinedByWhitespace) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const words = window.map((token) => token.value);
|
|
369
|
+
if (!isLikelyNameCandidate(words, preservedNames)) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
spans.push({
|
|
374
|
+
end: window[window.length - 1].end,
|
|
375
|
+
start: window[0].start,
|
|
376
|
+
});
|
|
377
|
+
index += size - 1;
|
|
378
|
+
matched = true;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (matched) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return spans
|
|
388
|
+
.sort((left, right) => right.start - left.start)
|
|
389
|
+
.reduce(
|
|
390
|
+
(output, span) =>
|
|
391
|
+
`${output.slice(0, span.start)}${PLACEHOLDERS.name}${output.slice(span.end)}`,
|
|
392
|
+
String(text)
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function replaceMatterLabelDerivedNames(text, replacements) {
|
|
397
|
+
return replacements
|
|
398
|
+
.slice()
|
|
399
|
+
.sort((left, right) => right.value.length - left.value.length)
|
|
400
|
+
.reduce((output, replacement) => {
|
|
401
|
+
const matcher = new RegExp(`\\b${escapeRegExp(replacement.value)}\\b`, "gi");
|
|
402
|
+
return output.replace(matcher, replacement.placeholder);
|
|
403
|
+
}, String(text));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function isMatterLabelContext(path, key) {
|
|
407
|
+
return path[path.length - 1] === "matter" && MATTER_LABEL_FIELDS.has(key);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function redactStringValue(
|
|
411
|
+
text,
|
|
412
|
+
key,
|
|
413
|
+
replacements,
|
|
414
|
+
preservedNames,
|
|
415
|
+
derivedLabelReplacements,
|
|
416
|
+
path
|
|
417
|
+
) {
|
|
418
|
+
let output = String(text);
|
|
419
|
+
|
|
420
|
+
output = replaceKnownSensitiveValues(output, replacements);
|
|
421
|
+
output = redactPatternPii(output);
|
|
422
|
+
|
|
423
|
+
if (isMatterLabelContext(path, key)) {
|
|
424
|
+
output = replaceMatterLabelDerivedNames(output, derivedLabelReplacements);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (FREE_TEXT_FIELDS.has(key) || LABEL_FIELDS.has(key)) {
|
|
428
|
+
output = redactLikelyBareNames(output, preservedNames);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return output;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function redactValue(
|
|
435
|
+
value,
|
|
436
|
+
resourceType,
|
|
437
|
+
contactLikeContext,
|
|
438
|
+
replacements,
|
|
439
|
+
preservedNames,
|
|
440
|
+
derivedLabelReplacements,
|
|
441
|
+
safeIdentityContext = false,
|
|
442
|
+
path = []
|
|
443
|
+
) {
|
|
444
|
+
if (Array.isArray(value)) {
|
|
445
|
+
return value.map((item) =>
|
|
446
|
+
redactValue(
|
|
447
|
+
item,
|
|
448
|
+
resourceType,
|
|
449
|
+
contactLikeContext,
|
|
450
|
+
replacements,
|
|
451
|
+
preservedNames,
|
|
452
|
+
derivedLabelReplacements,
|
|
453
|
+
safeIdentityContext,
|
|
454
|
+
path
|
|
455
|
+
)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!isObject(value)) {
|
|
460
|
+
return value;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const output = {};
|
|
464
|
+
|
|
465
|
+
Object.entries(value).forEach(([key, child]) => {
|
|
466
|
+
if (safeIdentityContext && (NAME_FIELDS.has(key) || EMAIL_FIELDS.has(key) || PHONE_FIELDS.has(key))) {
|
|
467
|
+
output[key] = child;
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (contactLikeContext && NAME_FIELDS.has(key)) {
|
|
472
|
+
output[key] = PLACEHOLDERS.name;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (contactLikeContext && EMAIL_FIELDS.has(key)) {
|
|
477
|
+
output[key] = PLACEHOLDERS.email;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (contactLikeContext && PHONE_FIELDS.has(key)) {
|
|
482
|
+
output[key] = PLACEHOLDERS.phone;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (typeof child === "string") {
|
|
487
|
+
output[key] = safeIdentityContext
|
|
488
|
+
? child
|
|
489
|
+
: redactStringValue(
|
|
490
|
+
child,
|
|
491
|
+
key,
|
|
492
|
+
replacements,
|
|
493
|
+
preservedNames,
|
|
494
|
+
derivedLabelReplacements,
|
|
495
|
+
path
|
|
496
|
+
);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
output[key] = redactValue(
|
|
501
|
+
child,
|
|
502
|
+
resourceType,
|
|
503
|
+
contactLikeContext || CLIENT_OBJECT_KEYS.has(key),
|
|
504
|
+
replacements,
|
|
505
|
+
preservedNames,
|
|
506
|
+
derivedLabelReplacements,
|
|
507
|
+
safeIdentityContext || SAFE_IDENTITY_OBJECT_KEYS.has(key),
|
|
508
|
+
path.concat(key)
|
|
509
|
+
);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
return output;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function redactPayload(value, resourceType) {
|
|
516
|
+
const replacements = collectSensitiveReplacements(value, resourceType);
|
|
517
|
+
const derivedLabelReplacements = collectPersonClientLabelReplacements(value);
|
|
518
|
+
const preservedNames = collectSafeIdentityNames(
|
|
519
|
+
value,
|
|
520
|
+
isSafeIdentityResourceType(resourceType)
|
|
521
|
+
);
|
|
522
|
+
return redactValue(
|
|
523
|
+
value,
|
|
524
|
+
resourceType,
|
|
525
|
+
isContactLikeResourceType(resourceType),
|
|
526
|
+
replacements,
|
|
527
|
+
preservedNames,
|
|
528
|
+
derivedLabelReplacements,
|
|
529
|
+
isSafeIdentityResourceType(resourceType)
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function maybeRedactData(data, options, resourceType) {
|
|
534
|
+
if (!options?.redacted) {
|
|
535
|
+
return data;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return redactPayload(data, resourceType);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function maybeRedactPayload(payload, options, resourceType) {
|
|
542
|
+
if (!options?.redacted || !isObject(payload) || !Object.hasOwn(payload, "data")) {
|
|
543
|
+
return payload;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
...payload,
|
|
548
|
+
data: redactPayload(payload.data, resourceType),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
module.exports = {
|
|
553
|
+
PLACEHOLDERS,
|
|
554
|
+
maybeRedactData,
|
|
555
|
+
maybeRedactPayload,
|
|
556
|
+
__private: {
|
|
557
|
+
collectPersonClientLabelReplacements,
|
|
558
|
+
collectSafeIdentityNames,
|
|
559
|
+
derivePersonSurname,
|
|
560
|
+
redactLikelyBareNames,
|
|
561
|
+
redactPatternPii,
|
|
562
|
+
collectSensitiveReplacements,
|
|
563
|
+
redactPayload,
|
|
564
|
+
replaceMatterLabelDerivedNames,
|
|
565
|
+
redactStringValue,
|
|
566
|
+
replaceKnownSensitiveValues,
|
|
567
|
+
},
|
|
568
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
function isLoopbackHostname(hostname) {
|
|
2
|
+
const normalized = String(hostname || "").trim().toLowerCase();
|
|
3
|
+
return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "[::1]";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function getLoopbackBindHost(hostname) {
|
|
7
|
+
const normalized = String(hostname || "").trim().toLowerCase();
|
|
8
|
+
|
|
9
|
+
if (normalized === "[::1]") {
|
|
10
|
+
return "::1";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseLoopbackRedirectUri(redirectUri) {
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = new URL(redirectUri);
|
|
20
|
+
} catch (_error) {
|
|
21
|
+
throw new Error("Redirect URI must be a valid URL.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (parsed.protocol !== "http:") {
|
|
25
|
+
throw new Error("Redirect URI must use http:// for the local OAuth callback.");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!isLoopbackHostname(parsed.hostname)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
"Redirect URI must use a loopback host (127.0.0.1, localhost, or [::1])."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!parsed.port) {
|
|
35
|
+
throw new Error("Redirect URI must include an explicit port for local OAuth login.");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (parsed.username || parsed.password) {
|
|
39
|
+
throw new Error("Redirect URI must not include embedded credentials.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (parsed.search || parsed.hash) {
|
|
43
|
+
throw new Error("Redirect URI must not include query parameters or fragments.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
getLoopbackBindHost,
|
|
51
|
+
isLoopbackHostname,
|
|
52
|
+
parseLoopbackRedirectUri,
|
|
53
|
+
};
|