@reproapp/node-sdk 0.0.3 → 0.0.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.
- package/README.md +36 -2
- package/dist/index.d.ts +140 -15
- package/dist/index.js +4927 -927
- package/dist/ingest/client.d.ts +10 -0
- package/dist/ingest/client.js +158 -0
- package/dist/ingest/mapper.d.ts +2 -0
- package/dist/ingest/mapper.js +92 -0
- package/dist/ingest/types.d.ts +40 -0
- package/dist/ingest/types.js +2 -0
- package/dist/ingest/worker.js +19 -0
- package/dist/integrations/sendgrid.d.ts +2 -4
- package/dist/integrations/sendgrid.js +4 -14
- package/dist/privacy-fallback.d.ts +1 -0
- package/dist/privacy-fallback.js +27 -0
- package/dist/privacy-redaction.d.ts +3 -0
- package/dist/privacy-redaction.js +38 -0
- package/dist/privacy.d.ts +108 -0
- package/dist/privacy.js +2868 -0
- package/dist/trace-materializer-worker.d.ts +1 -0
- package/dist/trace-materializer-worker.js +33 -0
- package/docs/tracing.md +1 -0
- package/package.json +8 -2
- package/src/index.ts +5583 -954
- package/src/ingest/client.ts +194 -0
- package/src/ingest/mapper.ts +104 -0
- package/src/ingest/types.ts +42 -0
- package/src/integrations/sendgrid.ts +6 -19
- package/src/privacy-fallback.ts +25 -0
- package/src/privacy-redaction.ts +37 -0
- package/src/privacy.ts +3593 -0
- package/src/trace-materializer-worker.ts +39 -0
- package/test/circular-capture.test.js +111 -0
- package/test/disable-subtree.test.js +154 -0
- package/test/integration-unawaited.js +183 -0
- package/test/kafka-runtime-privacy-policy.test.js +285 -0
- package/test/privacy-runtime-policy.test.js +2043 -0
- package/test/promise-map.test.js +72 -0
- package/test/unawaited.test.js +163 -0
- package/test/wrap-plugin-arrow-args.test.js +80 -0
- package/tracer/cjs-hook.js +0 -1
- package/tracer/wrap-plugin.js +96 -10
- package/dist/redaction.d.ts +0 -44
- package/dist/redaction.js +0 -167
- package/dist/server.js +0 -26
- /package/dist/{server.d.ts → ingest/worker.d.ts} +0 -0
package/dist/privacy.js
ADDED
|
@@ -0,0 +1,2868 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyRuntimePrivacyPolicyAsync = exports.applyRuntimePrivacyPolicy = exports.normalizeRuntimePrivacyPolicy = exports.fetchApprovedRuntimePrivacyPolicy = exports.resolvePrivacyApiBase = exports.normalizePrivacyEnvironment = exports.clearRuntimePrivacyProvenance = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const privacy_fallback_1 = require("./privacy-fallback");
|
|
6
|
+
const privacy_redaction_1 = require("./privacy-redaction");
|
|
7
|
+
const EMPTY_RUNTIME_PRIVACY_POLICY = {
|
|
8
|
+
statements: [],
|
|
9
|
+
rawTextHints: [],
|
|
10
|
+
};
|
|
11
|
+
const RUNTIME_PRIVACY_PROVENANCE_MAX_ENTRIES = 1000;
|
|
12
|
+
const RUNTIME_PRIVACY_PROVENANCE_MAX_SERIALIZED_LENGTH = 200000;
|
|
13
|
+
let objectRuntimePrivacyProvenance = new WeakMap();
|
|
14
|
+
const serializedRuntimePrivacyProvenance = new Map();
|
|
15
|
+
function clearRuntimePrivacyProvenance() {
|
|
16
|
+
objectRuntimePrivacyProvenance = new WeakMap();
|
|
17
|
+
serializedRuntimePrivacyProvenance.clear();
|
|
18
|
+
}
|
|
19
|
+
exports.clearRuntimePrivacyProvenance = clearRuntimePrivacyProvenance;
|
|
20
|
+
const ALL_SURFACES = [
|
|
21
|
+
'db.pk',
|
|
22
|
+
'db.before',
|
|
23
|
+
'db.after',
|
|
24
|
+
'db.query',
|
|
25
|
+
'db.resultMeta',
|
|
26
|
+
'db.error',
|
|
27
|
+
'request.headers',
|
|
28
|
+
'request.body',
|
|
29
|
+
'request.params',
|
|
30
|
+
'request.query',
|
|
31
|
+
'response.body',
|
|
32
|
+
'trace.args',
|
|
33
|
+
'trace.returnValue',
|
|
34
|
+
'trace.error',
|
|
35
|
+
];
|
|
36
|
+
const TARGET_PREFIXES = [
|
|
37
|
+
'outbound.http.headers',
|
|
38
|
+
'outbound.http.body',
|
|
39
|
+
'log.payload',
|
|
40
|
+
'request.headers',
|
|
41
|
+
'request.body',
|
|
42
|
+
'request.params',
|
|
43
|
+
'request.query',
|
|
44
|
+
'response.body',
|
|
45
|
+
'trace.returnValue',
|
|
46
|
+
'trace.args',
|
|
47
|
+
'trace.error',
|
|
48
|
+
'db.resultMeta',
|
|
49
|
+
'db.document',
|
|
50
|
+
'db.before',
|
|
51
|
+
'db.after',
|
|
52
|
+
'db.query',
|
|
53
|
+
'db.error',
|
|
54
|
+
'db.pk',
|
|
55
|
+
'request',
|
|
56
|
+
'response',
|
|
57
|
+
'trace',
|
|
58
|
+
'payload',
|
|
59
|
+
'config',
|
|
60
|
+
'env',
|
|
61
|
+
];
|
|
62
|
+
function normalizePrivacyEnvironment(input) {
|
|
63
|
+
const raw = input?.trim() ||
|
|
64
|
+
process.env.REPRO_PRIVACY_ENV ||
|
|
65
|
+
process.env.REPRO_ENV ||
|
|
66
|
+
process.env.NODE_ENV ||
|
|
67
|
+
'dev';
|
|
68
|
+
const normalized = raw.trim().toLowerCase();
|
|
69
|
+
if (normalized === 'development')
|
|
70
|
+
return 'dev';
|
|
71
|
+
if (normalized === 'production')
|
|
72
|
+
return 'prod';
|
|
73
|
+
if (normalized === 'staging' || normalized === 'stage')
|
|
74
|
+
return 'qa';
|
|
75
|
+
return normalized || 'dev';
|
|
76
|
+
}
|
|
77
|
+
exports.normalizePrivacyEnvironment = normalizePrivacyEnvironment;
|
|
78
|
+
function resolvePrivacyApiBase(cfg) {
|
|
79
|
+
const value = cfg?.apiBase ||
|
|
80
|
+
process.env.REPRO_PRIVACY_API_BASE ||
|
|
81
|
+
process.env.REPRO_API_BASE ||
|
|
82
|
+
null;
|
|
83
|
+
if (!value || !String(value).trim()) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return String(value).replace(/\/+$/, '');
|
|
87
|
+
}
|
|
88
|
+
exports.resolvePrivacyApiBase = resolvePrivacyApiBase;
|
|
89
|
+
const DEFAULT_RUNTIME_PRIVACY_FETCH_TIMEOUT_MS = 2500;
|
|
90
|
+
function resolveRuntimePrivacyFetchTimeoutMs(value) {
|
|
91
|
+
if (!Number.isFinite(value)) {
|
|
92
|
+
return DEFAULT_RUNTIME_PRIVACY_FETCH_TIMEOUT_MS;
|
|
93
|
+
}
|
|
94
|
+
const normalized = Math.trunc(Number(value));
|
|
95
|
+
if (normalized < 250) {
|
|
96
|
+
return 250;
|
|
97
|
+
}
|
|
98
|
+
return normalized;
|
|
99
|
+
}
|
|
100
|
+
async function fetchApprovedRuntimePrivacyPolicy(params) {
|
|
101
|
+
if (!params.appId || !params.appSecret || !params.apiBase) {
|
|
102
|
+
return {
|
|
103
|
+
status: 'disabled',
|
|
104
|
+
reason: !params.apiBase ? 'missing_config' : 'missing_credentials',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const url = `${params.apiBase}/v1/sdk/privacy-policy?environment=${encodeURIComponent(params.environment)}`;
|
|
108
|
+
const headers = {
|
|
109
|
+
accept: 'application/json',
|
|
110
|
+
'x-app-id': params.appId,
|
|
111
|
+
'x-app-secret': params.appSecret,
|
|
112
|
+
};
|
|
113
|
+
if (params.appName) {
|
|
114
|
+
headers['x-app-name'] = params.appName;
|
|
115
|
+
}
|
|
116
|
+
const controller = typeof AbortController !== 'undefined'
|
|
117
|
+
? new AbortController()
|
|
118
|
+
: null;
|
|
119
|
+
const timeoutMs = resolveRuntimePrivacyFetchTimeoutMs(params.fetchTimeoutMs);
|
|
120
|
+
const timeout = controller
|
|
121
|
+
? setTimeout(() => controller.abort(), timeoutMs)
|
|
122
|
+
: null;
|
|
123
|
+
try {
|
|
124
|
+
const response = await fetch(url, {
|
|
125
|
+
method: 'GET',
|
|
126
|
+
headers,
|
|
127
|
+
signal: controller?.signal,
|
|
128
|
+
});
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
return { status: 'http_error', httpStatus: response.status };
|
|
131
|
+
}
|
|
132
|
+
const payload = await response.json();
|
|
133
|
+
if (!payload?.enabled || !payload.policy) {
|
|
134
|
+
return { status: 'disabled', reason: 'no_policy' };
|
|
135
|
+
}
|
|
136
|
+
const policy = normalizeRuntimePrivacyPolicy(payload.policy);
|
|
137
|
+
if (!policy) {
|
|
138
|
+
return { status: 'disabled', reason: 'empty_policy' };
|
|
139
|
+
}
|
|
140
|
+
return { status: 'ok', policy };
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error?.name === 'AbortError') {
|
|
144
|
+
return { status: 'network_error', error: 'timeout' };
|
|
145
|
+
}
|
|
146
|
+
return { status: 'network_error', error: 'request_failed' };
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
if (timeout) {
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
exports.fetchApprovedRuntimePrivacyPolicy = fetchApprovedRuntimePrivacyPolicy;
|
|
155
|
+
function normalizeRuntimePrivacyPolicy(raw) {
|
|
156
|
+
if (!raw || typeof raw !== 'object') {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const policy = raw;
|
|
160
|
+
const flattened = flattenStatements(policy);
|
|
161
|
+
const rawTextHints = normalizeRawTextHints(policy.rawTextHints);
|
|
162
|
+
const statements = flattened
|
|
163
|
+
.map((entry, index) => normalizeStatement(entry, index))
|
|
164
|
+
.filter((entry) => entry !== null)
|
|
165
|
+
.sort((left, right) => left.priority - right.priority);
|
|
166
|
+
if (!statements.length) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
environment: typeof policy.environment === 'string' ? policy.environment : undefined,
|
|
171
|
+
strength: policy.strength === 'weak' || policy.strength === 'medium' || policy.strength === 'strict'
|
|
172
|
+
? policy.strength
|
|
173
|
+
: undefined,
|
|
174
|
+
statements,
|
|
175
|
+
rawTextHints,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
exports.normalizeRuntimePrivacyPolicy = normalizeRuntimePrivacyPolicy;
|
|
179
|
+
function applyRuntimePrivacyPolicy(policy, surface, value, context) {
|
|
180
|
+
if (value === undefined) {
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
return transformValue(policy ?? EMPTY_RUNTIME_PRIVACY_POLICY, surface, value, context, []);
|
|
184
|
+
}
|
|
185
|
+
exports.applyRuntimePrivacyPolicy = applyRuntimePrivacyPolicy;
|
|
186
|
+
async function applyRuntimePrivacyPolicyAsync(policy, surface, value, context) {
|
|
187
|
+
if (value === undefined) {
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
return transformValueAsync(policy ?? EMPTY_RUNTIME_PRIVACY_POLICY, surface, value, context, []);
|
|
191
|
+
}
|
|
192
|
+
exports.applyRuntimePrivacyPolicyAsync = applyRuntimePrivacyPolicyAsync;
|
|
193
|
+
function flattenStatements(policy) {
|
|
194
|
+
const fromGroups = Array.isArray(policy.groups)
|
|
195
|
+
? policy.groups.flatMap((group) => Array.isArray(group?.statements) ? group.statements : [])
|
|
196
|
+
: [];
|
|
197
|
+
if (fromGroups.length) {
|
|
198
|
+
return fromGroups;
|
|
199
|
+
}
|
|
200
|
+
return Array.isArray(policy.statements) ? policy.statements : [];
|
|
201
|
+
}
|
|
202
|
+
function normalizeStatement(input, index) {
|
|
203
|
+
const scope = input?.when?.scope;
|
|
204
|
+
const selector = typeof input?.when?.selector === 'string' ? input.when.selector.trim() : '';
|
|
205
|
+
const action = input?.apply?.action;
|
|
206
|
+
if (!selector ||
|
|
207
|
+
(scope !== 'FIELD' &&
|
|
208
|
+
scope !== 'FUNCTION' &&
|
|
209
|
+
scope !== 'ROUTE' &&
|
|
210
|
+
scope !== 'SCHEMA' &&
|
|
211
|
+
scope !== 'QUERY' &&
|
|
212
|
+
scope !== 'PATTERN') ||
|
|
213
|
+
(action !== 'KEEP_EXACT' && action !== 'TOKENIZE' && action !== 'SUMMARIZE' && action !== 'DROP')) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const pattern = typeof input?.when?.pattern === 'string' && input.when.pattern.trim()
|
|
217
|
+
? safeRegExp(input.when.pattern)
|
|
218
|
+
: undefined;
|
|
219
|
+
const targetMatchers = parseTargetMatchers(typeof input?.when?.target === 'string' ? input.when.target : '');
|
|
220
|
+
const selectorTokens = selector
|
|
221
|
+
.split('|')
|
|
222
|
+
.map((part) => part.trim())
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
.map((part) => (scope === 'FIELD' ? normalizeFieldSelectorToken(part) : part))
|
|
225
|
+
.filter(Boolean);
|
|
226
|
+
let selectorLeaves = selectorTokens
|
|
227
|
+
.map((part) => {
|
|
228
|
+
const normalized = part.replace(/\s*->.*$/, '').trim();
|
|
229
|
+
const lastDot = normalized.lastIndexOf('.');
|
|
230
|
+
return lastDot >= 0 ? normalized.slice(lastDot + 1) : normalized;
|
|
231
|
+
})
|
|
232
|
+
.filter(Boolean);
|
|
233
|
+
if (scope === 'FIELD' && targetMatchers.length) {
|
|
234
|
+
const targetLeaves = targetMatchers
|
|
235
|
+
.flatMap((matcher) => {
|
|
236
|
+
const leaf = matcher.path?.[matcher.path.length - 1] ?? '';
|
|
237
|
+
if (!leaf || leaf === '*' || /^\d+$/.test(leaf)) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
return [leaf];
|
|
241
|
+
});
|
|
242
|
+
if (targetLeaves.length) {
|
|
243
|
+
selectorLeaves = Array.from(new Set([...selectorLeaves, ...targetLeaves]));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
sid: typeof input.sid === 'string' ? input.sid : `${String(scope).toLowerCase()}-${index + 1}`,
|
|
248
|
+
scope,
|
|
249
|
+
selector,
|
|
250
|
+
action,
|
|
251
|
+
priority: typeof input?.meta?.priority === 'number' && Number.isFinite(input.meta.priority)
|
|
252
|
+
? input.meta.priority
|
|
253
|
+
: index + 1,
|
|
254
|
+
pattern,
|
|
255
|
+
selectorTokens,
|
|
256
|
+
selectorLeaves,
|
|
257
|
+
targetMatchers,
|
|
258
|
+
textPolicy: normalizeTextPolicy(input?.apply?.textPolicy ?? input?.textPolicy),
|
|
259
|
+
rawTextMode: normalizeRawTextMode(input?.apply?.rawTextMode ?? input?.rawTextMode),
|
|
260
|
+
rawTextHintNames: normalizeRawTextHintNames(input?.meta?.review?.rawTextHintNames),
|
|
261
|
+
descendantRawTextMode: normalizeRawTextMode(input?.apply?.descendantRawTextMode ?? input?.descendantRawTextMode),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function resolveEffectiveAction(baseAction, value, path) {
|
|
265
|
+
const safetyFloor = deriveSensitiveLeafFloorAction(value, path);
|
|
266
|
+
if (!safetyFloor) {
|
|
267
|
+
return baseAction;
|
|
268
|
+
}
|
|
269
|
+
if (!baseAction) {
|
|
270
|
+
return safetyFloor;
|
|
271
|
+
}
|
|
272
|
+
if (baseAction === 'KEEP_EXACT') {
|
|
273
|
+
return safetyFloor === 'SUMMARIZE' ? baseAction : safetyFloor;
|
|
274
|
+
}
|
|
275
|
+
if (baseAction === 'DROP') {
|
|
276
|
+
return baseAction;
|
|
277
|
+
}
|
|
278
|
+
if (safetyFloor === 'DROP') {
|
|
279
|
+
return 'DROP';
|
|
280
|
+
}
|
|
281
|
+
if (safetyFloor === 'TOKENIZE' && baseAction === 'SUMMARIZE') {
|
|
282
|
+
return 'TOKENIZE';
|
|
283
|
+
}
|
|
284
|
+
return baseAction;
|
|
285
|
+
}
|
|
286
|
+
function deriveSensitiveLeafFloorAction(value, path) {
|
|
287
|
+
if (!path.length) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
if (value !== null && typeof value === 'object') {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
const comparablePath = normalizeComparablePath(path);
|
|
294
|
+
if (!comparablePath) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
if (DROP_SAFETY_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
298
|
+
return 'DROP';
|
|
299
|
+
}
|
|
300
|
+
if (TOKENIZE_SAFETY_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
301
|
+
return 'TOKENIZE';
|
|
302
|
+
}
|
|
303
|
+
if (typeof value === 'string') {
|
|
304
|
+
if (looksLikeEmail(value)) {
|
|
305
|
+
return 'TOKENIZE';
|
|
306
|
+
}
|
|
307
|
+
if (looksLikeConnectionString(value)) {
|
|
308
|
+
return 'DROP';
|
|
309
|
+
}
|
|
310
|
+
if (FREE_TEXT_FALLBACK_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
311
|
+
return 'SUMMARIZE';
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
function transformValue(policy, surface, value, context, path, fallbackInput = null) {
|
|
317
|
+
const fallback = resolveTransformFallback(value, fallbackInput);
|
|
318
|
+
const objectId = coercePrivacyObjectId(value);
|
|
319
|
+
if (objectId !== null) {
|
|
320
|
+
value = objectId;
|
|
321
|
+
}
|
|
322
|
+
value = normalizeEncodedCollectionForPrivacy(value);
|
|
323
|
+
const statement = matchStatement(policy, surface, value, context, path);
|
|
324
|
+
if (statement?.textPolicy?.mode === 'KNOWN_TEMPLATE' && typeof value === 'string') {
|
|
325
|
+
return applyKnownTextPolicy(statement.textPolicy, value, context, surface, path);
|
|
326
|
+
}
|
|
327
|
+
let currentValue = value;
|
|
328
|
+
const action = resolveEffectiveAction(statement?.action ?? fallback?.action, currentValue, path);
|
|
329
|
+
const deferKnownStringDetectors = shouldDeferKnownStringDetectorsToRawHints(statement, fallback);
|
|
330
|
+
if (typeof currentValue === 'string' && action !== 'DROP' && action !== 'TOKENIZE') {
|
|
331
|
+
const structured = transformSerializedStructuredString(policy, surface, currentValue, context, path, serializedStringFallbackForStatement(statement, action, fallback));
|
|
332
|
+
if (structured !== null) {
|
|
333
|
+
return structured;
|
|
334
|
+
}
|
|
335
|
+
if (!deferKnownStringDetectors) {
|
|
336
|
+
const detected = sanitizeTextWithKnownDetectorsSync(currentValue, path);
|
|
337
|
+
if (detected !== currentValue && (action === 'KEEP_EXACT' || !action) && !statement?.descendantRawTextMode) {
|
|
338
|
+
return detected;
|
|
339
|
+
}
|
|
340
|
+
currentValue = detected;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (typeof currentValue === 'string' && (action === 'DROP' || action === 'TOKENIZE')) {
|
|
344
|
+
const structured = transformSerializedStructuredStringAsChildren(policy, surface, currentValue, context, path);
|
|
345
|
+
if (structured !== null) {
|
|
346
|
+
return structured;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (shouldPreserveBroadContainerSurface(statement, action, surface, currentValue, path)) {
|
|
350
|
+
return transformChildrenExact(policy, surface, currentValue, context, path, childFallbackForExact(statement, action, fallback));
|
|
351
|
+
}
|
|
352
|
+
if (typeof currentValue === 'string' && action === 'KEEP_EXACT' && statement?.descendantRawTextMode) {
|
|
353
|
+
return summarizeValue(policy, currentValue, context, surface, path, { rawTextMode: statement.descendantRawTextMode });
|
|
354
|
+
}
|
|
355
|
+
if (action === 'KEEP_EXACT') {
|
|
356
|
+
return transformChildrenExact(policy, surface, currentValue, context, path, childFallbackForExact(statement, action, fallback));
|
|
357
|
+
}
|
|
358
|
+
if (action === 'DROP') {
|
|
359
|
+
return '[dropped]';
|
|
360
|
+
}
|
|
361
|
+
if (action === 'TOKENIZE') {
|
|
362
|
+
if (isStructuredPrivacyContainer(currentValue)) {
|
|
363
|
+
return transformChildrenWithFallback(policy, surface, currentValue, context, path, 'TOKENIZE');
|
|
364
|
+
}
|
|
365
|
+
return tokenizeValue(currentValue, context, surface, path);
|
|
366
|
+
}
|
|
367
|
+
if (action === 'SUMMARIZE') {
|
|
368
|
+
const summaryPolicy = summaryPolicyFor(statement, fallback);
|
|
369
|
+
const summaryFallback = summaryFallbackForPolicy(summaryPolicy);
|
|
370
|
+
if (isStructuredPrivacyContainer(currentValue) && summaryFallback) {
|
|
371
|
+
return transformChildrenExact(policy, surface, currentValue, context, path, summaryFallback);
|
|
372
|
+
}
|
|
373
|
+
if (!statement && fallback?.action === 'SUMMARIZE' && isStructuredPrivacyContainer(currentValue)) {
|
|
374
|
+
return transformChildrenExact(policy, surface, currentValue, context, path, fallback);
|
|
375
|
+
}
|
|
376
|
+
return summarizeValue(policy, currentValue, context, surface, path, summaryPolicy);
|
|
377
|
+
}
|
|
378
|
+
return transformChildrenExact(policy, surface, currentValue, context, path, fallback);
|
|
379
|
+
}
|
|
380
|
+
async function transformValueAsync(policy, surface, value, context, path, fallbackInput = null) {
|
|
381
|
+
const fallback = resolveTransformFallback(value, fallbackInput);
|
|
382
|
+
const objectId = coercePrivacyObjectId(value);
|
|
383
|
+
if (objectId !== null) {
|
|
384
|
+
value = objectId;
|
|
385
|
+
}
|
|
386
|
+
value = normalizeEncodedCollectionForPrivacy(value);
|
|
387
|
+
const statement = matchStatement(policy, surface, value, context, path);
|
|
388
|
+
if (statement?.textPolicy?.mode === 'KNOWN_TEMPLATE' && typeof value === 'string') {
|
|
389
|
+
return applyKnownTextPolicy(statement.textPolicy, value, context, surface, path);
|
|
390
|
+
}
|
|
391
|
+
let currentValue = value;
|
|
392
|
+
const action = resolveEffectiveAction(statement?.action ?? fallback?.action, currentValue, path);
|
|
393
|
+
const deferKnownStringDetectors = shouldDeferKnownStringDetectorsToRawHints(statement, fallback);
|
|
394
|
+
let syncDetectorChanged = false;
|
|
395
|
+
if (typeof currentValue === 'string' && action !== 'DROP' && action !== 'TOKENIZE') {
|
|
396
|
+
const structured = await transformSerializedStructuredStringAsync(policy, surface, currentValue, context, path, serializedStringFallbackForStatement(statement, action, fallback));
|
|
397
|
+
if (structured !== null) {
|
|
398
|
+
return structured;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (typeof currentValue === 'string' && action !== 'DROP' && action !== 'TOKENIZE' && !deferKnownStringDetectors) {
|
|
402
|
+
const syncDetected = sanitizeTextWithKnownDetectorsSync(currentValue, path);
|
|
403
|
+
if (syncDetected !== currentValue) {
|
|
404
|
+
currentValue = syncDetected;
|
|
405
|
+
syncDetectorChanged = true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (typeof currentValue === 'string' &&
|
|
409
|
+
shouldRunAsyncStringDetector(statement, fallback, currentValue, path) &&
|
|
410
|
+
action !== 'DROP' &&
|
|
411
|
+
action !== 'TOKENIZE' &&
|
|
412
|
+
!deferKnownStringDetectors) {
|
|
413
|
+
const skipAsyncDetector = syncDetectorChanged && !hasPotentialSensitiveLeak(currentValue);
|
|
414
|
+
if (!skipAsyncDetector) {
|
|
415
|
+
const detected = await (0, privacy_fallback_1.sanitizeUnknownBoundaryTextWithDetectors)(currentValue);
|
|
416
|
+
if (detected !== currentValue) {
|
|
417
|
+
currentValue = detected;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (typeof currentValue === 'string' && (action === 'DROP' || action === 'TOKENIZE')) {
|
|
422
|
+
const structured = await transformSerializedStructuredStringAsChildrenAsync(policy, surface, currentValue, context, path);
|
|
423
|
+
if (structured !== null) {
|
|
424
|
+
return structured;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (shouldPreserveBroadContainerSurface(statement, action, surface, currentValue, path)) {
|
|
428
|
+
return transformChildrenExactAsync(policy, surface, currentValue, context, path, childFallbackForExact(statement, action, fallback));
|
|
429
|
+
}
|
|
430
|
+
if (typeof currentValue === 'string' && action === 'KEEP_EXACT' && statement?.descendantRawTextMode) {
|
|
431
|
+
return summarizeValueAsync(policy, currentValue, context, surface, path, { rawTextMode: statement.descendantRawTextMode });
|
|
432
|
+
}
|
|
433
|
+
if (action === 'KEEP_EXACT') {
|
|
434
|
+
return transformChildrenExactAsync(policy, surface, currentValue, context, path, childFallbackForExact(statement, action, fallback));
|
|
435
|
+
}
|
|
436
|
+
if (action === 'DROP') {
|
|
437
|
+
return '[dropped]';
|
|
438
|
+
}
|
|
439
|
+
if (action === 'TOKENIZE') {
|
|
440
|
+
if (isStructuredPrivacyContainer(currentValue)) {
|
|
441
|
+
return transformChildrenWithFallbackAsync(policy, surface, currentValue, context, path, 'TOKENIZE');
|
|
442
|
+
}
|
|
443
|
+
return tokenizeValue(currentValue, context, surface, path);
|
|
444
|
+
}
|
|
445
|
+
if (action === 'SUMMARIZE') {
|
|
446
|
+
const summaryPolicy = summaryPolicyFor(statement, fallback);
|
|
447
|
+
const summaryFallback = summaryFallbackForPolicy(summaryPolicy);
|
|
448
|
+
if (isStructuredPrivacyContainer(currentValue) && summaryFallback) {
|
|
449
|
+
return transformChildrenExactAsync(policy, surface, currentValue, context, path, summaryFallback);
|
|
450
|
+
}
|
|
451
|
+
if (!statement && fallback?.action === 'SUMMARIZE' && isStructuredPrivacyContainer(currentValue)) {
|
|
452
|
+
return transformChildrenExactAsync(policy, surface, currentValue, context, path, fallback);
|
|
453
|
+
}
|
|
454
|
+
return summarizeValueAsync(policy, currentValue, context, surface, path, summaryPolicy);
|
|
455
|
+
}
|
|
456
|
+
return transformChildrenExactAsync(policy, surface, currentValue, context, path, fallback);
|
|
457
|
+
}
|
|
458
|
+
function resolveTransformFallback(value, input) {
|
|
459
|
+
const explicit = normalizeTransformFallback(input);
|
|
460
|
+
if (explicit) {
|
|
461
|
+
rememberRuntimePrivacyProvenance(value, explicit);
|
|
462
|
+
return explicit;
|
|
463
|
+
}
|
|
464
|
+
return getRuntimePrivacyProvenance(value);
|
|
465
|
+
}
|
|
466
|
+
function normalizeTransformFallback(input) {
|
|
467
|
+
if (!input) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
if (typeof input === 'string') {
|
|
471
|
+
return { action: input };
|
|
472
|
+
}
|
|
473
|
+
return input;
|
|
474
|
+
}
|
|
475
|
+
function childFallbackForExact(statement, action, inherited) {
|
|
476
|
+
if (statement) {
|
|
477
|
+
if (action === 'KEEP_EXACT' && statement.descendantRawTextMode) {
|
|
478
|
+
const mergedRawTextMode = mergeRawTextMode(statement.descendantRawTextMode, inherited?.rawTextMode);
|
|
479
|
+
const inheritedWins = inherited !== null &&
|
|
480
|
+
inherited.rawTextMode === mergedRawTextMode &&
|
|
481
|
+
inherited.rawTextMode !== statement.descendantRawTextMode;
|
|
482
|
+
return {
|
|
483
|
+
action: 'SUMMARIZE',
|
|
484
|
+
rawTextMode: mergedRawTextMode,
|
|
485
|
+
rawTextHintNames: inheritedWins ? inherited.rawTextHintNames : statement.rawTextHintNames,
|
|
486
|
+
sourceSid: inheritedWins ? inherited.sourceSid : statement.sid,
|
|
487
|
+
sourceSelector: inheritedWins ? inherited.sourceSelector : statement.selector,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return action === 'KEEP_EXACT' ? inherited : null;
|
|
491
|
+
}
|
|
492
|
+
return inherited;
|
|
493
|
+
}
|
|
494
|
+
function summaryPolicyFor(statement, fallback) {
|
|
495
|
+
if (statement) {
|
|
496
|
+
if (statement.textPolicy) {
|
|
497
|
+
return statement;
|
|
498
|
+
}
|
|
499
|
+
const rawTextMode = mergeRawTextMode(statement.rawTextMode, fallback?.rawTextMode);
|
|
500
|
+
return rawTextMode ? { ...statement, rawTextMode } : statement;
|
|
501
|
+
}
|
|
502
|
+
return fallback?.rawTextMode
|
|
503
|
+
? { rawTextMode: fallback.rawTextMode, rawTextHintNames: fallback.rawTextHintNames }
|
|
504
|
+
: null;
|
|
505
|
+
}
|
|
506
|
+
function shouldDeferKnownStringDetectorsToRawHints(statement, fallback) {
|
|
507
|
+
return mergeRawTextMode(statement?.rawTextMode, fallback?.rawTextMode) === 'REGEX_ASSISTED_EXACT';
|
|
508
|
+
}
|
|
509
|
+
function mergeRawTextMode(primary, inherited) {
|
|
510
|
+
if (primary === 'REGEX_ASSISTED_EXACT' || inherited === 'REGEX_ASSISTED_EXACT') {
|
|
511
|
+
return 'REGEX_ASSISTED_EXACT';
|
|
512
|
+
}
|
|
513
|
+
return primary ?? inherited;
|
|
514
|
+
}
|
|
515
|
+
function summaryFallbackForPolicy(policy) {
|
|
516
|
+
return policy?.rawTextMode
|
|
517
|
+
? { action: 'SUMMARIZE', rawTextMode: policy.rawTextMode, rawTextHintNames: policy.rawTextHintNames }
|
|
518
|
+
: null;
|
|
519
|
+
}
|
|
520
|
+
function serializedStringFallbackForStatement(statement, action, inherited) {
|
|
521
|
+
if (!statement || action === 'DROP' || action === 'TOKENIZE') {
|
|
522
|
+
return inherited;
|
|
523
|
+
}
|
|
524
|
+
const rawTextMode = mergeRawTextMode(statement.rawTextMode, inherited?.rawTextMode);
|
|
525
|
+
if (!rawTextMode) {
|
|
526
|
+
return inherited;
|
|
527
|
+
}
|
|
528
|
+
const inheritedWins = inherited !== null &&
|
|
529
|
+
inherited.rawTextMode === rawTextMode &&
|
|
530
|
+
inherited.rawTextMode !== statement.rawTextMode;
|
|
531
|
+
return {
|
|
532
|
+
action: 'SUMMARIZE',
|
|
533
|
+
rawTextMode,
|
|
534
|
+
rawTextHintNames: inheritedWins ? inherited.rawTextHintNames : statement.rawTextHintNames,
|
|
535
|
+
sourceSid: inheritedWins ? inherited.sourceSid : statement.sid,
|
|
536
|
+
sourceSelector: inheritedWins ? inherited.sourceSelector : statement.selector,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function isRuntimePrivacyProvenanceFallback(fallback) {
|
|
540
|
+
return Boolean(fallback && fallback.action === 'SUMMARIZE' && fallback.rawTextMode);
|
|
541
|
+
}
|
|
542
|
+
function rememberRuntimePrivacyProvenance(value, fallback) {
|
|
543
|
+
if (!isRuntimePrivacyProvenanceFallback(fallback)) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const provenance = {
|
|
547
|
+
action: fallback.action,
|
|
548
|
+
rawTextMode: fallback.rawTextMode,
|
|
549
|
+
sourceSid: fallback.sourceSid,
|
|
550
|
+
sourceSelector: fallback.sourceSelector,
|
|
551
|
+
inherited: true,
|
|
552
|
+
};
|
|
553
|
+
if (value && typeof value === 'object') {
|
|
554
|
+
objectRuntimePrivacyProvenance.set(value, provenance);
|
|
555
|
+
}
|
|
556
|
+
rememberSerializedRuntimePrivacyProvenance(value, provenance);
|
|
557
|
+
}
|
|
558
|
+
function getRuntimePrivacyProvenance(value) {
|
|
559
|
+
if (value && typeof value === 'object') {
|
|
560
|
+
const objectProvenance = objectRuntimePrivacyProvenance.get(value);
|
|
561
|
+
if (objectProvenance) {
|
|
562
|
+
return objectProvenance;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return lookupSerializedRuntimePrivacyProvenance(value);
|
|
566
|
+
}
|
|
567
|
+
function rememberSerializedRuntimePrivacyProvenance(value, provenance) {
|
|
568
|
+
for (const serialized of runtimePrivacyProvenanceSerializations(value)) {
|
|
569
|
+
const key = hashRuntimePrivacyProvenance(serialized);
|
|
570
|
+
if (!serializedRuntimePrivacyProvenance.has(key) && serializedRuntimePrivacyProvenance.size >= RUNTIME_PRIVACY_PROVENANCE_MAX_ENTRIES) {
|
|
571
|
+
const first = serializedRuntimePrivacyProvenance.keys().next().value;
|
|
572
|
+
if (first) {
|
|
573
|
+
serializedRuntimePrivacyProvenance.delete(first);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
serializedRuntimePrivacyProvenance.set(key, provenance);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function lookupSerializedRuntimePrivacyProvenance(value) {
|
|
580
|
+
for (const serialized of runtimePrivacyProvenanceSerializations(value)) {
|
|
581
|
+
const provenance = serializedRuntimePrivacyProvenance.get(hashRuntimePrivacyProvenance(serialized));
|
|
582
|
+
if (provenance) {
|
|
583
|
+
return provenance;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
function runtimePrivacyProvenanceSerializations(value) {
|
|
589
|
+
if (value === undefined || value === null) {
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
if (typeof value === 'string') {
|
|
593
|
+
if (!value || value.length > RUNTIME_PRIVACY_PROVENANCE_MAX_SERIALIZED_LENGTH) {
|
|
594
|
+
return [];
|
|
595
|
+
}
|
|
596
|
+
const out = [value];
|
|
597
|
+
const parsed = parseSerializedStructuredText(value);
|
|
598
|
+
if (parsed !== null) {
|
|
599
|
+
const stable = stableStringifyForRuntimePrivacyProvenance(parsed);
|
|
600
|
+
if (stable && stable !== value) {
|
|
601
|
+
out.push(stable);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return out;
|
|
605
|
+
}
|
|
606
|
+
if (!isStructuredPrivacyContainer(value)) {
|
|
607
|
+
return [];
|
|
608
|
+
}
|
|
609
|
+
const out = [];
|
|
610
|
+
const json = jsonStringifyForRuntimePrivacyProvenance(value);
|
|
611
|
+
if (json) {
|
|
612
|
+
out.push(json);
|
|
613
|
+
}
|
|
614
|
+
const stable = stableStringifyForRuntimePrivacyProvenance(value);
|
|
615
|
+
if (stable && stable !== json) {
|
|
616
|
+
out.push(stable);
|
|
617
|
+
}
|
|
618
|
+
return out;
|
|
619
|
+
}
|
|
620
|
+
function jsonStringifyForRuntimePrivacyProvenance(value) {
|
|
621
|
+
try {
|
|
622
|
+
const serialized = JSON.stringify(value);
|
|
623
|
+
return serialized && serialized.length <= RUNTIME_PRIVACY_PROVENANCE_MAX_SERIALIZED_LENGTH
|
|
624
|
+
? serialized
|
|
625
|
+
: null;
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function stableStringifyForRuntimePrivacyProvenance(value) {
|
|
632
|
+
try {
|
|
633
|
+
const seen = new WeakSet();
|
|
634
|
+
const serialized = stableStringifyForRuntimePrivacyProvenanceInner(value, seen);
|
|
635
|
+
return serialized && serialized.length <= RUNTIME_PRIVACY_PROVENANCE_MAX_SERIALIZED_LENGTH
|
|
636
|
+
? serialized
|
|
637
|
+
: null;
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function stableStringifyForRuntimePrivacyProvenanceInner(value, seen) {
|
|
644
|
+
if (value === undefined)
|
|
645
|
+
return 'undefined';
|
|
646
|
+
if (value === null)
|
|
647
|
+
return 'null';
|
|
648
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
649
|
+
return String(value);
|
|
650
|
+
}
|
|
651
|
+
if (typeof value === 'bigint') {
|
|
652
|
+
return `${value.toString()}n`;
|
|
653
|
+
}
|
|
654
|
+
if (typeof value !== 'object') {
|
|
655
|
+
return String(value);
|
|
656
|
+
}
|
|
657
|
+
if (value instanceof Date) {
|
|
658
|
+
return value.toISOString();
|
|
659
|
+
}
|
|
660
|
+
if (value instanceof Error || isWeakCollection(value)) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
if (seen.has(value)) {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
seen.add(value);
|
|
667
|
+
try {
|
|
668
|
+
if (Array.isArray(value)) {
|
|
669
|
+
const items = value.map((item) => stableStringifyForRuntimePrivacyProvenanceInner(item, seen));
|
|
670
|
+
if (items.some((item) => item === null)) {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
return `[${items.join(',')}]`;
|
|
674
|
+
}
|
|
675
|
+
if (value instanceof Map) {
|
|
676
|
+
const entries = Array.from(value.entries())
|
|
677
|
+
.map(([key, child], index) => [
|
|
678
|
+
normalizeCollectionKeyForPrivacy(key, index),
|
|
679
|
+
stableStringifyForRuntimePrivacyProvenanceInner(child, seen),
|
|
680
|
+
]);
|
|
681
|
+
if (entries.some(([, child]) => child === null)) {
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
return `{${entries.sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => `${key}:${child}`).join(',')}}`;
|
|
685
|
+
}
|
|
686
|
+
if (value instanceof Set) {
|
|
687
|
+
const items = Array.from(value.values()).map((item) => stableStringifyForRuntimePrivacyProvenanceInner(item, seen));
|
|
688
|
+
if (items.some((item) => item === null)) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
return `[${items.join(',')}]`;
|
|
692
|
+
}
|
|
693
|
+
const keys = Object.keys(value).sort();
|
|
694
|
+
const fields = [];
|
|
695
|
+
for (const key of keys) {
|
|
696
|
+
const child = stableStringifyForRuntimePrivacyProvenanceInner(value[key], seen);
|
|
697
|
+
if (child === null) {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
fields.push(`${key}:${child}`);
|
|
701
|
+
}
|
|
702
|
+
return `{${fields.join(',')}}`;
|
|
703
|
+
}
|
|
704
|
+
finally {
|
|
705
|
+
seen.delete(value);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function hashRuntimePrivacyProvenance(serialized) {
|
|
709
|
+
return (0, crypto_1.createHash)('sha256').update(`runtime-privacy-provenance|${serialized}`).digest('hex');
|
|
710
|
+
}
|
|
711
|
+
function transformChildrenExact(policy, surface, value, context, path, fallback = null) {
|
|
712
|
+
rememberRuntimePrivacyProvenance(value, fallback);
|
|
713
|
+
if (Array.isArray(value)) {
|
|
714
|
+
return value.map((item, index) => transformValue(policy, surface, item, context, path.concat(String(index)), fallback));
|
|
715
|
+
}
|
|
716
|
+
if (value instanceof Map) {
|
|
717
|
+
const output = {};
|
|
718
|
+
let index = 0;
|
|
719
|
+
for (const [key, child] of value.entries()) {
|
|
720
|
+
const normalizedKey = normalizeCollectionKeyForPrivacy(key, index);
|
|
721
|
+
output[normalizedKey] = transformValue(policy, surface, child, context, path.concat(normalizedKey), fallback);
|
|
722
|
+
index += 1;
|
|
723
|
+
}
|
|
724
|
+
return output;
|
|
725
|
+
}
|
|
726
|
+
if (value instanceof Set) {
|
|
727
|
+
return Array.from(value.values()).map((item, index) => transformValue(policy, surface, item, context, path.concat(String(index)), fallback));
|
|
728
|
+
}
|
|
729
|
+
if (isWeakCollection(value)) {
|
|
730
|
+
return `[${getPrivacyConstructorName(value)}: uninspectable]`;
|
|
731
|
+
}
|
|
732
|
+
if (value && typeof value === 'object') {
|
|
733
|
+
if (value instanceof Date || value instanceof Error) {
|
|
734
|
+
return value;
|
|
735
|
+
}
|
|
736
|
+
const output = {};
|
|
737
|
+
for (const [key, child] of Object.entries(value)) {
|
|
738
|
+
output[key] = transformValue(policy, surface, child, context, path.concat(key), fallback);
|
|
739
|
+
}
|
|
740
|
+
return output;
|
|
741
|
+
}
|
|
742
|
+
return value;
|
|
743
|
+
}
|
|
744
|
+
function transformChildrenWithFallback(policy, surface, value, context, path, fallbackAction) {
|
|
745
|
+
if (Array.isArray(value)) {
|
|
746
|
+
return value.map((item, index) => transformValue(policy, surface, item, context, path.concat(String(index)), fallbackAction));
|
|
747
|
+
}
|
|
748
|
+
if (value instanceof Map) {
|
|
749
|
+
const output = {};
|
|
750
|
+
let index = 0;
|
|
751
|
+
for (const [key, child] of value.entries()) {
|
|
752
|
+
const normalizedKey = normalizeCollectionKeyForPrivacy(key, index);
|
|
753
|
+
output[normalizedKey] = transformValue(policy, surface, child, context, path.concat(normalizedKey), fallbackAction);
|
|
754
|
+
index += 1;
|
|
755
|
+
}
|
|
756
|
+
return output;
|
|
757
|
+
}
|
|
758
|
+
if (value instanceof Set) {
|
|
759
|
+
return Array.from(value.values()).map((item, index) => transformValue(policy, surface, item, context, path.concat(String(index)), fallbackAction));
|
|
760
|
+
}
|
|
761
|
+
if (isWeakCollection(value)) {
|
|
762
|
+
return transformValue(policy, surface, `[${getPrivacyConstructorName(value)}: uninspectable]`, context, path, fallbackAction);
|
|
763
|
+
}
|
|
764
|
+
if (value && typeof value === 'object') {
|
|
765
|
+
if (value instanceof Date || value instanceof Error) {
|
|
766
|
+
return transformValue(policy, surface, String(value), context, path, fallbackAction);
|
|
767
|
+
}
|
|
768
|
+
const output = {};
|
|
769
|
+
for (const [key, child] of Object.entries(value)) {
|
|
770
|
+
output[key] = transformValue(policy, surface, child, context, path.concat(key), fallbackAction);
|
|
771
|
+
}
|
|
772
|
+
return output;
|
|
773
|
+
}
|
|
774
|
+
return transformValue(policy, surface, value, context, path, fallbackAction);
|
|
775
|
+
}
|
|
776
|
+
async function transformChildrenExactAsync(policy, surface, value, context, path, fallback = null) {
|
|
777
|
+
rememberRuntimePrivacyProvenance(value, fallback);
|
|
778
|
+
if (Array.isArray(value)) {
|
|
779
|
+
return Promise.all(value.map((item, index) => transformValueAsync(policy, surface, item, context, path.concat(String(index)), fallback)));
|
|
780
|
+
}
|
|
781
|
+
if (value instanceof Map) {
|
|
782
|
+
const output = {};
|
|
783
|
+
let index = 0;
|
|
784
|
+
for (const [key, child] of value.entries()) {
|
|
785
|
+
const normalizedKey = normalizeCollectionKeyForPrivacy(key, index);
|
|
786
|
+
output[normalizedKey] = await transformValueAsync(policy, surface, child, context, path.concat(normalizedKey), fallback);
|
|
787
|
+
index += 1;
|
|
788
|
+
}
|
|
789
|
+
return output;
|
|
790
|
+
}
|
|
791
|
+
if (value instanceof Set) {
|
|
792
|
+
return Promise.all(Array.from(value.values()).map((item, index) => transformValueAsync(policy, surface, item, context, path.concat(String(index)), fallback)));
|
|
793
|
+
}
|
|
794
|
+
if (isWeakCollection(value)) {
|
|
795
|
+
return `[${getPrivacyConstructorName(value)}: uninspectable]`;
|
|
796
|
+
}
|
|
797
|
+
if (value && typeof value === 'object') {
|
|
798
|
+
if (value instanceof Date || value instanceof Error) {
|
|
799
|
+
return value;
|
|
800
|
+
}
|
|
801
|
+
const output = {};
|
|
802
|
+
for (const [key, child] of Object.entries(value)) {
|
|
803
|
+
output[key] = await transformValueAsync(policy, surface, child, context, path.concat(key), fallback);
|
|
804
|
+
}
|
|
805
|
+
return output;
|
|
806
|
+
}
|
|
807
|
+
return value;
|
|
808
|
+
}
|
|
809
|
+
async function transformChildrenWithFallbackAsync(policy, surface, value, context, path, fallbackAction) {
|
|
810
|
+
if (Array.isArray(value)) {
|
|
811
|
+
return Promise.all(value.map((item, index) => transformValueAsync(policy, surface, item, context, path.concat(String(index)), fallbackAction)));
|
|
812
|
+
}
|
|
813
|
+
if (value instanceof Map) {
|
|
814
|
+
const output = {};
|
|
815
|
+
let index = 0;
|
|
816
|
+
for (const [key, child] of value.entries()) {
|
|
817
|
+
const normalizedKey = normalizeCollectionKeyForPrivacy(key, index);
|
|
818
|
+
output[normalizedKey] = await transformValueAsync(policy, surface, child, context, path.concat(normalizedKey), fallbackAction);
|
|
819
|
+
index += 1;
|
|
820
|
+
}
|
|
821
|
+
return output;
|
|
822
|
+
}
|
|
823
|
+
if (value instanceof Set) {
|
|
824
|
+
return Promise.all(Array.from(value.values()).map((item, index) => transformValueAsync(policy, surface, item, context, path.concat(String(index)), fallbackAction)));
|
|
825
|
+
}
|
|
826
|
+
if (isWeakCollection(value)) {
|
|
827
|
+
return transformValueAsync(policy, surface, `[${getPrivacyConstructorName(value)}: uninspectable]`, context, path, fallbackAction);
|
|
828
|
+
}
|
|
829
|
+
if (value && typeof value === 'object') {
|
|
830
|
+
if (value instanceof Date || value instanceof Error) {
|
|
831
|
+
return transformValueAsync(policy, surface, String(value), context, path, fallbackAction);
|
|
832
|
+
}
|
|
833
|
+
const output = {};
|
|
834
|
+
for (const [key, child] of Object.entries(value)) {
|
|
835
|
+
output[key] = await transformValueAsync(policy, surface, child, context, path.concat(key), fallbackAction);
|
|
836
|
+
}
|
|
837
|
+
return output;
|
|
838
|
+
}
|
|
839
|
+
return transformValueAsync(policy, surface, value, context, path, fallbackAction);
|
|
840
|
+
}
|
|
841
|
+
function isStructuredPrivacyContainer(value) {
|
|
842
|
+
return (value !== null &&
|
|
843
|
+
typeof value === 'object' &&
|
|
844
|
+
!(value instanceof Date) &&
|
|
845
|
+
!(value instanceof Error));
|
|
846
|
+
}
|
|
847
|
+
function normalizeCollectionKeyForPrivacy(value, index) {
|
|
848
|
+
const objectId = coercePrivacyObjectId(value);
|
|
849
|
+
if (objectId !== null) {
|
|
850
|
+
return objectId;
|
|
851
|
+
}
|
|
852
|
+
if (typeof value === 'string' && value) {
|
|
853
|
+
return value;
|
|
854
|
+
}
|
|
855
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
856
|
+
return String(value);
|
|
857
|
+
}
|
|
858
|
+
if (typeof value === 'symbol') {
|
|
859
|
+
return value.description ? `symbol:${value.description}` : `symbol:${index}`;
|
|
860
|
+
}
|
|
861
|
+
return `mapEntry${index}`;
|
|
862
|
+
}
|
|
863
|
+
function isWeakCollection(value) {
|
|
864
|
+
return value instanceof WeakMap || value instanceof WeakSet;
|
|
865
|
+
}
|
|
866
|
+
function getPrivacyConstructorName(value) {
|
|
867
|
+
return value?.constructor?.name || 'Object';
|
|
868
|
+
}
|
|
869
|
+
function normalizeEncodedCollectionForPrivacy(value) {
|
|
870
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
871
|
+
return value;
|
|
872
|
+
}
|
|
873
|
+
const type = typeof value.__type === 'string' ? value.__type : '';
|
|
874
|
+
if (type === 'Map' && Array.isArray(value.entries)) {
|
|
875
|
+
const output = {};
|
|
876
|
+
value.entries.forEach((entry, index) => {
|
|
877
|
+
if (!Array.isArray(entry) || !entry.length) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
output[normalizeCollectionKeyForPrivacy(entry[0], index)] = entry[1];
|
|
881
|
+
});
|
|
882
|
+
return output;
|
|
883
|
+
}
|
|
884
|
+
if (type === 'Set' && Array.isArray(value.values)) {
|
|
885
|
+
return value.values;
|
|
886
|
+
}
|
|
887
|
+
return value;
|
|
888
|
+
}
|
|
889
|
+
function transformSerializedStructuredString(policy, surface, value, context, path, fallback = null) {
|
|
890
|
+
const parsed = parseSerializedStructuredText(value);
|
|
891
|
+
if (parsed === null) {
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
const transformed = transformValue(policy, surface, parsed, context, path, fallback);
|
|
895
|
+
if (stableStringify(transformed) === stableStringify(parsed)) {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
return JSON.stringify(transformed);
|
|
899
|
+
}
|
|
900
|
+
async function transformSerializedStructuredStringAsync(policy, surface, value, context, path, fallback = null) {
|
|
901
|
+
const parsed = parseSerializedStructuredText(value);
|
|
902
|
+
if (parsed === null) {
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
const transformed = await transformValueAsync(policy, surface, parsed, context, path, fallback);
|
|
906
|
+
if (stableStringify(transformed) === stableStringify(parsed)) {
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
return JSON.stringify(transformed);
|
|
910
|
+
}
|
|
911
|
+
function transformSerializedStructuredStringAsChildren(policy, surface, value, context, path) {
|
|
912
|
+
const parsed = parseSerializedStructuredText(value);
|
|
913
|
+
if (parsed === null) {
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
const transformed = transformChildrenExact(policy, surface, parsed, context, path);
|
|
917
|
+
if (stableStringify(transformed) === stableStringify(parsed)) {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
return JSON.stringify(transformed);
|
|
921
|
+
}
|
|
922
|
+
async function transformSerializedStructuredStringAsChildrenAsync(policy, surface, value, context, path) {
|
|
923
|
+
const parsed = parseSerializedStructuredText(value);
|
|
924
|
+
if (parsed === null) {
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
const transformed = await transformChildrenExactAsync(policy, surface, parsed, context, path);
|
|
928
|
+
if (stableStringify(transformed) === stableStringify(parsed)) {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
return JSON.stringify(transformed);
|
|
932
|
+
}
|
|
933
|
+
function matchStatement(policy, surface, value, context, path) {
|
|
934
|
+
let candidates = [];
|
|
935
|
+
for (const statement of policy.statements) {
|
|
936
|
+
if (shouldIgnoreBroadContainerRuleOnDescendant(statement, surface, path)) {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
let targetScore = bestTargetMatchScore(statement.targetMatchers, surface, path);
|
|
940
|
+
if (targetScore < 0 && statement.scope === 'FIELD') {
|
|
941
|
+
targetScore = relaxedFieldTargetMatchScore(statement, surface, path);
|
|
942
|
+
}
|
|
943
|
+
if (targetScore < 0) {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (!matchesScope(statement, surface, value, context, path)) {
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
const selectorScore = matchSelectorSpecificity(statement, surface, context, path);
|
|
950
|
+
candidates.push({
|
|
951
|
+
statement,
|
|
952
|
+
targetScore,
|
|
953
|
+
score: targetScore * 1000 +
|
|
954
|
+
selectorScore +
|
|
955
|
+
semanticScopeSpecificity(statement) +
|
|
956
|
+
statementSpecificity(statement) -
|
|
957
|
+
statement.priority,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
if (!candidates.length) {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
candidates = candidates.filter(({ statement }) => !isBroadContextKeepExact(statement, surface, path));
|
|
964
|
+
if (!candidates.length) {
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
const textPolicyCandidates = typeof value === 'string'
|
|
968
|
+
? candidates.filter(({ statement }) => statement.textPolicy?.mode === 'KNOWN_TEMPLATE')
|
|
969
|
+
: [];
|
|
970
|
+
if (textPolicyCandidates.length) {
|
|
971
|
+
candidates.length = 0;
|
|
972
|
+
candidates.push(...textPolicyCandidates);
|
|
973
|
+
}
|
|
974
|
+
const strongestProtectiveTargetScore = Math.max(-1, ...candidates
|
|
975
|
+
.filter(({ statement }) => statement.action !== 'KEEP_EXACT')
|
|
976
|
+
.map(({ targetScore }) => targetScore));
|
|
977
|
+
if (strongestProtectiveTargetScore >= 0) {
|
|
978
|
+
candidates = candidates.filter(({ statement, targetScore }) => statement.action !== 'KEEP_EXACT' || targetScore > strongestProtectiveTargetScore);
|
|
979
|
+
}
|
|
980
|
+
const hasNestedTransform = path.length > 0 && candidates.some(({ statement, targetScore }) => statement.action !== 'KEEP_EXACT' &&
|
|
981
|
+
(targetScore > 0 || statement.scope === 'FIELD' || statement.scope === 'PATTERN'));
|
|
982
|
+
const active = hasNestedTransform
|
|
983
|
+
? candidates.filter((candidate) => !isBroadContextKeepExact(candidate.statement, surface, path))
|
|
984
|
+
: candidates;
|
|
985
|
+
const pool = active.length ? active : candidates;
|
|
986
|
+
let best = null;
|
|
987
|
+
for (const candidate of pool) {
|
|
988
|
+
if (!best || candidate.score > best.score) {
|
|
989
|
+
best = { statement: candidate.statement, score: candidate.score };
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return best?.statement ?? null;
|
|
993
|
+
}
|
|
994
|
+
function semanticScopeSpecificity(statement) {
|
|
995
|
+
switch (statement.scope) {
|
|
996
|
+
case 'FIELD':
|
|
997
|
+
return 350;
|
|
998
|
+
case 'PATTERN':
|
|
999
|
+
return 325;
|
|
1000
|
+
case 'SCHEMA':
|
|
1001
|
+
case 'QUERY':
|
|
1002
|
+
return 250;
|
|
1003
|
+
default:
|
|
1004
|
+
return 0;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function actionSpecificity(action) {
|
|
1008
|
+
switch (action) {
|
|
1009
|
+
case 'DROP':
|
|
1010
|
+
return 300;
|
|
1011
|
+
case 'TOKENIZE':
|
|
1012
|
+
return 200;
|
|
1013
|
+
case 'SUMMARIZE':
|
|
1014
|
+
return 100;
|
|
1015
|
+
case 'KEEP_EXACT':
|
|
1016
|
+
default:
|
|
1017
|
+
return 0;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function statementSpecificity(statement) {
|
|
1021
|
+
let score = actionSpecificity(statement.action);
|
|
1022
|
+
if (statement.textPolicy?.mode === 'KNOWN_TEMPLATE') {
|
|
1023
|
+
score += 90;
|
|
1024
|
+
}
|
|
1025
|
+
score += rawTextModeSpecificity(statement.rawTextMode);
|
|
1026
|
+
score += rawTextModeSpecificity(statement.descendantRawTextMode);
|
|
1027
|
+
return score;
|
|
1028
|
+
}
|
|
1029
|
+
function rawTextModeSpecificity(mode) {
|
|
1030
|
+
if (mode === 'REGEX_ASSISTED_EXACT') {
|
|
1031
|
+
return 80;
|
|
1032
|
+
}
|
|
1033
|
+
if (mode === 'SAFE_PARTIAL_MASK') {
|
|
1034
|
+
return 10;
|
|
1035
|
+
}
|
|
1036
|
+
return 0;
|
|
1037
|
+
}
|
|
1038
|
+
function shouldPreserveBroadContainerSurface(statement, action, surface, value, path) {
|
|
1039
|
+
if (!statement || !action || action === 'KEEP_EXACT') {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
if (action === 'TOKENIZE') {
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
if (value === null || value === undefined || typeof value !== 'object' || value instanceof Date || value instanceof Error) {
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
if (!hasStructuredContainerTargetMatch(statement.targetMatchers, surface, path)) {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
return ((statement.scope === 'FUNCTION' && surface.startsWith('trace.')) ||
|
|
1052
|
+
(statement.scope === 'ROUTE' && (surface.startsWith('request.') || surface === 'response.body')) ||
|
|
1053
|
+
((statement.scope === 'QUERY' || statement.scope === 'SCHEMA') && surface.startsWith('db.')));
|
|
1054
|
+
}
|
|
1055
|
+
function shouldIgnoreBroadContainerRuleOnDescendant(statement, surface, path) {
|
|
1056
|
+
if (!path.length || statement.action === 'KEEP_EXACT') {
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
if (!hasStructuredContainerAncestorTarget(statement.targetMatchers, surface, path)) {
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1062
|
+
return ((statement.scope === 'FUNCTION' && surface.startsWith('trace.')) ||
|
|
1063
|
+
(statement.scope === 'ROUTE' && (surface.startsWith('request.') || surface === 'response.body')) ||
|
|
1064
|
+
((statement.scope === 'QUERY' || statement.scope === 'SCHEMA') && surface.startsWith('db.')));
|
|
1065
|
+
}
|
|
1066
|
+
function matchSelectorSpecificity(statement, surface, context, path) {
|
|
1067
|
+
switch (statement.scope) {
|
|
1068
|
+
case 'FUNCTION': {
|
|
1069
|
+
const fn = context.trace?.fn?.trim();
|
|
1070
|
+
if (!fn) {
|
|
1071
|
+
return 0;
|
|
1072
|
+
}
|
|
1073
|
+
const normalizedFn = normalizeFunctionSelector(fn);
|
|
1074
|
+
let best = 0;
|
|
1075
|
+
for (const selector of statement.selectorTokens) {
|
|
1076
|
+
if (!functionSelectorMatches(selector, fn)) {
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
const normalizedSelector = normalizeFunctionSelector(selector);
|
|
1080
|
+
const dotCount = normalizedSelector.split('.').filter(Boolean).length;
|
|
1081
|
+
const score = 400 + dotCount * 25 - (statement.selectorTokens.length - 1) * 50;
|
|
1082
|
+
best = Math.max(best, score);
|
|
1083
|
+
}
|
|
1084
|
+
return best || (normalizedFn ? 1 : 0);
|
|
1085
|
+
}
|
|
1086
|
+
case 'FIELD': {
|
|
1087
|
+
const leaf = path[path.length - 1] ?? '';
|
|
1088
|
+
const joinedPath = normalizeComparablePath(path);
|
|
1089
|
+
let best = 0;
|
|
1090
|
+
for (const selector of statement.selectorLeaves) {
|
|
1091
|
+
if (!tokenMatchesPathToken(selector, leaf, joinedPath)) {
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
const normalizedSelector = normalizeComparableToken(selector);
|
|
1095
|
+
const dotCount = normalizedSelector.split('.').filter(Boolean).length;
|
|
1096
|
+
best = Math.max(best, 150 + dotCount * 15 - (statement.selectorLeaves.length - 1) * 10);
|
|
1097
|
+
}
|
|
1098
|
+
return best;
|
|
1099
|
+
}
|
|
1100
|
+
case 'ROUTE':
|
|
1101
|
+
return 200 - (statement.selectorTokens.length - 1) * 25;
|
|
1102
|
+
default:
|
|
1103
|
+
return 0;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function isBroadContextKeepExact(statement, surface, path) {
|
|
1107
|
+
if (statement.action !== 'KEEP_EXACT') {
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
if (statement.textPolicy?.mode === 'KNOWN_TEMPLATE') {
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
if (!path.length) {
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
if (hasSpecificExactTargetMatch(statement.targetMatchers, surface, path)) {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
return (statement.scope === 'FIELD' ||
|
|
1120
|
+
statement.scope === 'FUNCTION' ||
|
|
1121
|
+
statement.scope === 'ROUTE' ||
|
|
1122
|
+
statement.scope === 'SCHEMA' ||
|
|
1123
|
+
statement.scope === 'QUERY');
|
|
1124
|
+
}
|
|
1125
|
+
function hasSpecificExactTargetMatch(matchers, surface, path) {
|
|
1126
|
+
return matchers.some((matcher) => {
|
|
1127
|
+
if (!matcher.surfaces.includes(surface) || !matcher.path || matcher.path.length !== path.length) {
|
|
1128
|
+
return false;
|
|
1129
|
+
}
|
|
1130
|
+
return matcher.path.every((segment, index) => segment === path[index]);
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
function matchesScope(statement, surface, value, context, path) {
|
|
1134
|
+
const leaf = path[path.length - 1] ?? '';
|
|
1135
|
+
const joinedPath = normalizeComparablePath(path);
|
|
1136
|
+
switch (statement.scope) {
|
|
1137
|
+
case 'FIELD':
|
|
1138
|
+
return matchesFieldScope(statement, surface, path, leaf, joinedPath);
|
|
1139
|
+
case 'FUNCTION': {
|
|
1140
|
+
if (!surface.startsWith('trace.'))
|
|
1141
|
+
return false;
|
|
1142
|
+
const fn = context.trace?.fn?.trim();
|
|
1143
|
+
if (!fn)
|
|
1144
|
+
return false;
|
|
1145
|
+
return statement.selectorTokens.some((selector) => functionSelectorMatches(selector, fn));
|
|
1146
|
+
}
|
|
1147
|
+
case 'ROUTE':
|
|
1148
|
+
return Boolean(context.routeKey &&
|
|
1149
|
+
statement.selectorTokens.some((selector) => routeSelectorMatches(selector, context.routeKey)));
|
|
1150
|
+
case 'SCHEMA':
|
|
1151
|
+
return schemaSelectorMatches(statement.selectorTokens, surface, context, leaf, joinedPath);
|
|
1152
|
+
case 'QUERY':
|
|
1153
|
+
return querySelectorMatches(statement.selectorTokens, surface, context);
|
|
1154
|
+
case 'PATTERN':
|
|
1155
|
+
return patternMatches(statement.pattern, leaf, joinedPath, value);
|
|
1156
|
+
default:
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function matchesFieldScope(statement, surface, path, leaf, joinedPath) {
|
|
1161
|
+
const selectorMatches = statement.selectorLeaves.some((token) => tokenMatchesPathToken(token, leaf, joinedPath));
|
|
1162
|
+
if (!statement.targetMatchers.length) {
|
|
1163
|
+
return selectorMatches;
|
|
1164
|
+
}
|
|
1165
|
+
const hasSpecificTargetMatch = statement.targetMatchers.some((matcher) => {
|
|
1166
|
+
if (!matcher.surfaces.includes(surface) || !matcher.path || !matcher.path.length) {
|
|
1167
|
+
return false;
|
|
1168
|
+
}
|
|
1169
|
+
return pathPatternMatches(matcher.path, path);
|
|
1170
|
+
});
|
|
1171
|
+
if (hasSpecificTargetMatch) {
|
|
1172
|
+
return true;
|
|
1173
|
+
}
|
|
1174
|
+
return selectorMatches;
|
|
1175
|
+
}
|
|
1176
|
+
function hasStructuredContainerTargetMatch(matchers, surface, path) {
|
|
1177
|
+
return matchers.some((matcher) => {
|
|
1178
|
+
if (!matcher.surfaces.includes(surface)) {
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
if (!matcher.path || !matcher.path.length) {
|
|
1182
|
+
return path.length === 0;
|
|
1183
|
+
}
|
|
1184
|
+
if (matcher.path.length !== path.length) {
|
|
1185
|
+
return false;
|
|
1186
|
+
}
|
|
1187
|
+
return matcher.path.every((segment, index) => segment === path[index]);
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
function hasStructuredContainerAncestorTarget(matchers, surface, path) {
|
|
1191
|
+
return matchers.some((matcher) => {
|
|
1192
|
+
if (!matcher.surfaces.includes(surface)) {
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
if (!matcher.path || !matcher.path.length) {
|
|
1196
|
+
return true;
|
|
1197
|
+
}
|
|
1198
|
+
if (matcher.path.length >= path.length) {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
return matcher.path.every((segment, index) => segment === path[index]);
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
function schemaSelectorMatches(selectors, surface, context, leaf, joinedPath) {
|
|
1205
|
+
if (surface.startsWith('db.')) {
|
|
1206
|
+
const collection = String(context.db?.collection ?? '').toLowerCase();
|
|
1207
|
+
if (collection && selectors.some((selector) => normalizeSchemaSelector(selector).toLowerCase() === collection)) {
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return selectors.some((selector) => {
|
|
1212
|
+
const normalized = normalizeSchemaSelector(selector);
|
|
1213
|
+
if (tokenMatchesPathToken(normalized, leaf, joinedPath)) {
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
const fieldToken = normalized
|
|
1217
|
+
.split(/[.\s/]+/)
|
|
1218
|
+
.map((part) => part.trim())
|
|
1219
|
+
.filter(Boolean)
|
|
1220
|
+
.pop();
|
|
1221
|
+
return fieldToken ? tokenMatchesPathToken(fieldToken, leaf, joinedPath) : false;
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
function querySelectorMatches(selectors, surface, context) {
|
|
1225
|
+
if (!surface.startsWith('db.')) {
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
const haystack = [
|
|
1229
|
+
String(context.db?.type ?? ''),
|
|
1230
|
+
String(context.db?.collection ?? ''),
|
|
1231
|
+
String(context.db?.op ?? ''),
|
|
1232
|
+
String(context.trace?.fn ?? ''),
|
|
1233
|
+
]
|
|
1234
|
+
.join(' ')
|
|
1235
|
+
.toLowerCase();
|
|
1236
|
+
return selectors.some((selector) => {
|
|
1237
|
+
const normalized = normalizeQuerySelector(selector).trim().toLowerCase();
|
|
1238
|
+
return normalized && haystack.includes(normalized);
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
function patternMatches(pattern, leaf, joinedPath, value) {
|
|
1242
|
+
if (!pattern)
|
|
1243
|
+
return false;
|
|
1244
|
+
if (leaf && pattern.test(leaf))
|
|
1245
|
+
return true;
|
|
1246
|
+
if (joinedPath && pattern.test(joinedPath))
|
|
1247
|
+
return true;
|
|
1248
|
+
if (typeof value === 'string' && pattern.test(value))
|
|
1249
|
+
return true;
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
function functionSelectorMatches(selector, fn) {
|
|
1253
|
+
const normalizedSelector = normalizeFunctionSelector(selector);
|
|
1254
|
+
const normalizedFn = normalizeFunctionSelector(fn);
|
|
1255
|
+
if (!normalizedSelector || !normalizedFn) {
|
|
1256
|
+
return false;
|
|
1257
|
+
}
|
|
1258
|
+
return normalizedFn === normalizedSelector
|
|
1259
|
+
|| normalizedFn.endsWith(`.${normalizedSelector}`)
|
|
1260
|
+
|| normalizedSelector.endsWith(`.${normalizedFn}`);
|
|
1261
|
+
}
|
|
1262
|
+
function normalizeFunctionSelector(value) {
|
|
1263
|
+
return value
|
|
1264
|
+
.trim()
|
|
1265
|
+
.replace(/^fn:/i, '')
|
|
1266
|
+
.replace(/^function[.:]/i, '')
|
|
1267
|
+
.replace(/^method[.:]/i, '')
|
|
1268
|
+
.replace(/(\.args.*|\.returnValue.*|\.return.*|\.error.*|\.log.*)$/i, '')
|
|
1269
|
+
.trim();
|
|
1270
|
+
}
|
|
1271
|
+
function normalizeRouteSelector(value) {
|
|
1272
|
+
return value.trim().replace(/^route:/i, '').trim();
|
|
1273
|
+
}
|
|
1274
|
+
function routeSelectorMatches(selector, routeKey) {
|
|
1275
|
+
const normalizedSelector = normalizeRouteSelector(selector);
|
|
1276
|
+
const normalizedRouteKey = routeKey.trim();
|
|
1277
|
+
if (!normalizedSelector || !normalizedRouteKey) {
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
if (normalizedSelector === normalizedRouteKey) {
|
|
1281
|
+
return true;
|
|
1282
|
+
}
|
|
1283
|
+
const escaped = normalizedSelector
|
|
1284
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
1285
|
+
.replace(/:[A-Za-z_][A-Za-z0-9_]*/g, '[^\\s/]+');
|
|
1286
|
+
try {
|
|
1287
|
+
return new RegExp(`^${escaped}$`).test(normalizedRouteKey);
|
|
1288
|
+
}
|
|
1289
|
+
catch {
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
function normalizeSchemaSelector(value) {
|
|
1294
|
+
return value.trim().replace(/^schema:/i, '').trim();
|
|
1295
|
+
}
|
|
1296
|
+
function normalizeQuerySelector(value) {
|
|
1297
|
+
return value.trim().replace(/^query:/i, '').trim();
|
|
1298
|
+
}
|
|
1299
|
+
function normalizeFieldSelectorToken(value) {
|
|
1300
|
+
return value
|
|
1301
|
+
.trim()
|
|
1302
|
+
.replace(/^field\s*:/i, '')
|
|
1303
|
+
.replace(/^leaf\s*:/i, '')
|
|
1304
|
+
.replace(/^path\s*:/i, '')
|
|
1305
|
+
.replace(/^key\s*:/i, '')
|
|
1306
|
+
.replace(/^property\s*:/i, '')
|
|
1307
|
+
.replace(/^prop\s*:/i, '')
|
|
1308
|
+
.trim();
|
|
1309
|
+
}
|
|
1310
|
+
function tokenMatchesPathToken(token, leaf, joinedPath) {
|
|
1311
|
+
const normalizedToken = normalizeComparableToken(token);
|
|
1312
|
+
if (!normalizedToken)
|
|
1313
|
+
return false;
|
|
1314
|
+
if (normalizeComparableToken(leaf) === normalizedToken)
|
|
1315
|
+
return true;
|
|
1316
|
+
return joinedPath === normalizedToken || joinedPath.endsWith(`.${normalizedToken}`);
|
|
1317
|
+
}
|
|
1318
|
+
function normalizeComparablePath(path) {
|
|
1319
|
+
return path
|
|
1320
|
+
.filter((segment) => segment !== '*' && !/^\d+$/.test(segment))
|
|
1321
|
+
.map(normalizeComparableToken)
|
|
1322
|
+
.filter(Boolean)
|
|
1323
|
+
.join('.');
|
|
1324
|
+
}
|
|
1325
|
+
function normalizeComparableToken(value) {
|
|
1326
|
+
return value
|
|
1327
|
+
.trim()
|
|
1328
|
+
.replace(/\s+/g, '')
|
|
1329
|
+
.replace(/\[\*\]/g, '')
|
|
1330
|
+
.replace(/\[\d+\]/g, '')
|
|
1331
|
+
.replace(/^\$\.?/, '')
|
|
1332
|
+
.replace(/^.*->/, '')
|
|
1333
|
+
.replace(/\|/g, '')
|
|
1334
|
+
.toLowerCase();
|
|
1335
|
+
}
|
|
1336
|
+
function relaxedFieldTargetMatchScore(statement, surface, path) {
|
|
1337
|
+
if (!path.length) {
|
|
1338
|
+
return -1;
|
|
1339
|
+
}
|
|
1340
|
+
const leaf = path[path.length - 1] ?? '';
|
|
1341
|
+
const joinedPath = normalizeComparablePath(path);
|
|
1342
|
+
const leafMatches = statement.selectorLeaves.some((token) => tokenMatchesPathToken(token, leaf, joinedPath));
|
|
1343
|
+
if (!leafMatches) {
|
|
1344
|
+
return -1;
|
|
1345
|
+
}
|
|
1346
|
+
if (statement.targetMatchers.length && !statement.targetMatchers.some((matcher) => matcher.surfaces.includes(surface))) {
|
|
1347
|
+
return statement.action !== 'KEEP_EXACT' || statement.textPolicy ? 15 : -1;
|
|
1348
|
+
}
|
|
1349
|
+
let best = 25;
|
|
1350
|
+
for (const matcher of statement.targetMatchers) {
|
|
1351
|
+
if (!matcher.surfaces.includes(surface)) {
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
best = Math.max(best, relaxedSuffixSpecificity(matcher.path, path));
|
|
1355
|
+
}
|
|
1356
|
+
return best;
|
|
1357
|
+
}
|
|
1358
|
+
function relaxedSuffixSpecificity(pattern, path) {
|
|
1359
|
+
if (!pattern || !pattern.length) {
|
|
1360
|
+
return 25;
|
|
1361
|
+
}
|
|
1362
|
+
const normalizedPattern = pattern.filter((segment) => segment !== '*' && !/^\d+$/.test(segment));
|
|
1363
|
+
const normalizedPath = path.filter((segment) => segment !== '*' && !/^\d+$/.test(segment));
|
|
1364
|
+
if (!normalizedPattern.length || !normalizedPath.length) {
|
|
1365
|
+
return 25;
|
|
1366
|
+
}
|
|
1367
|
+
let expectedIndex = normalizedPattern.length - 1;
|
|
1368
|
+
let actualIndex = normalizedPath.length - 1;
|
|
1369
|
+
let matched = 0;
|
|
1370
|
+
while (expectedIndex >= 0 && actualIndex >= 0) {
|
|
1371
|
+
if (normalizedPattern[expectedIndex] === normalizedPath[actualIndex]) {
|
|
1372
|
+
matched += 1;
|
|
1373
|
+
expectedIndex -= 1;
|
|
1374
|
+
actualIndex -= 1;
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
actualIndex -= 1;
|
|
1378
|
+
}
|
|
1379
|
+
if (!matched) {
|
|
1380
|
+
return 25;
|
|
1381
|
+
}
|
|
1382
|
+
return 25 + matched * 10;
|
|
1383
|
+
}
|
|
1384
|
+
function matchesTarget(matchers, surface, path) {
|
|
1385
|
+
if (!matchers.length) {
|
|
1386
|
+
return true;
|
|
1387
|
+
}
|
|
1388
|
+
return matchers.some((matcher) => {
|
|
1389
|
+
if (!matcher.surfaces.includes(surface)) {
|
|
1390
|
+
return false;
|
|
1391
|
+
}
|
|
1392
|
+
if (!matcher.path || !matcher.path.length) {
|
|
1393
|
+
return true;
|
|
1394
|
+
}
|
|
1395
|
+
return pathPatternMatches(matcher.path, path);
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
function bestTargetMatchScore(matchers, surface, path) {
|
|
1399
|
+
if (!matchers.length) {
|
|
1400
|
+
return 0;
|
|
1401
|
+
}
|
|
1402
|
+
let best = -1;
|
|
1403
|
+
for (const matcher of matchers) {
|
|
1404
|
+
if (!matcher.surfaces.includes(surface)) {
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
if (!matcher.path || !matcher.path.length) {
|
|
1408
|
+
best = Math.max(best, 0);
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
const specificity = pathPatternSpecificity(matcher.path, path);
|
|
1412
|
+
best = Math.max(best, specificity);
|
|
1413
|
+
}
|
|
1414
|
+
return best;
|
|
1415
|
+
}
|
|
1416
|
+
function parseTargetMatchers(target) {
|
|
1417
|
+
if (!target.trim()) {
|
|
1418
|
+
return [];
|
|
1419
|
+
}
|
|
1420
|
+
return target
|
|
1421
|
+
.split(/[|,+]/)
|
|
1422
|
+
.map((part) => part.trim())
|
|
1423
|
+
.filter(Boolean)
|
|
1424
|
+
.flatMap(expandBraceExpression)
|
|
1425
|
+
.map(parseTargetMatcher)
|
|
1426
|
+
.filter((matcher) => matcher !== null);
|
|
1427
|
+
}
|
|
1428
|
+
function parseTargetMatcher(raw) {
|
|
1429
|
+
const token = raw.trim();
|
|
1430
|
+
if (!token) {
|
|
1431
|
+
return null;
|
|
1432
|
+
}
|
|
1433
|
+
for (const prefix of TARGET_PREFIXES) {
|
|
1434
|
+
if (token === prefix || token.startsWith(`${prefix}.`) || token.startsWith(`${prefix}[`)) {
|
|
1435
|
+
return {
|
|
1436
|
+
surfaces: resolveTargetSurfaces(prefix),
|
|
1437
|
+
path: token === prefix ? null : parsePath(token.slice(prefix.length + (token[prefix.length] === '[' ? 0 : 1))),
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return {
|
|
1442
|
+
surfaces: ALL_SURFACES,
|
|
1443
|
+
path: parsePath(token),
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function resolveTargetSurfaces(prefix) {
|
|
1447
|
+
switch (prefix) {
|
|
1448
|
+
case 'request':
|
|
1449
|
+
return ['request.headers', 'request.body', 'request.params', 'request.query'];
|
|
1450
|
+
case 'response':
|
|
1451
|
+
return ['response.body'];
|
|
1452
|
+
case 'trace':
|
|
1453
|
+
case 'log.payload':
|
|
1454
|
+
return ['trace.args', 'trace.returnValue', 'trace.error'];
|
|
1455
|
+
case 'db.document':
|
|
1456
|
+
case 'payload':
|
|
1457
|
+
return [
|
|
1458
|
+
'db.before',
|
|
1459
|
+
'db.after',
|
|
1460
|
+
'db.query',
|
|
1461
|
+
'db.resultMeta',
|
|
1462
|
+
'request.body',
|
|
1463
|
+
'response.body',
|
|
1464
|
+
'trace.args',
|
|
1465
|
+
'trace.returnValue',
|
|
1466
|
+
];
|
|
1467
|
+
case 'request.headers':
|
|
1468
|
+
case 'request.body':
|
|
1469
|
+
case 'request.params':
|
|
1470
|
+
case 'request.query':
|
|
1471
|
+
case 'response.body':
|
|
1472
|
+
case 'trace.args':
|
|
1473
|
+
case 'trace.returnValue':
|
|
1474
|
+
case 'trace.error':
|
|
1475
|
+
case 'db.pk':
|
|
1476
|
+
case 'db.before':
|
|
1477
|
+
case 'db.after':
|
|
1478
|
+
case 'db.query':
|
|
1479
|
+
case 'db.resultMeta':
|
|
1480
|
+
case 'db.error':
|
|
1481
|
+
return [prefix];
|
|
1482
|
+
default:
|
|
1483
|
+
return ALL_SURFACES;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
function pathPatternMatches(pattern, path) {
|
|
1487
|
+
return pathPatternSpecificity(pattern, path) >= 0;
|
|
1488
|
+
}
|
|
1489
|
+
function pathPatternSpecificity(pattern, path) {
|
|
1490
|
+
if (!pattern.length) {
|
|
1491
|
+
return 0;
|
|
1492
|
+
}
|
|
1493
|
+
if (pattern.length > path.length) {
|
|
1494
|
+
return -1;
|
|
1495
|
+
}
|
|
1496
|
+
let specificity = 0;
|
|
1497
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
1498
|
+
const expected = pattern[index];
|
|
1499
|
+
const actual = path[index];
|
|
1500
|
+
if (expected === '*') {
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
if (expected !== actual) {
|
|
1504
|
+
return -1;
|
|
1505
|
+
}
|
|
1506
|
+
specificity += 1;
|
|
1507
|
+
}
|
|
1508
|
+
return specificity * 100 + pattern.length;
|
|
1509
|
+
}
|
|
1510
|
+
function parsePath(value) {
|
|
1511
|
+
return value
|
|
1512
|
+
.trim()
|
|
1513
|
+
.replace(/^\./, '')
|
|
1514
|
+
.replace(/\[(\d+|\*)?\]/g, (_match, part) => `.${part || '*'}`)
|
|
1515
|
+
.split('.')
|
|
1516
|
+
.map((part) => part.trim())
|
|
1517
|
+
.filter(Boolean);
|
|
1518
|
+
}
|
|
1519
|
+
function expandBraceExpression(value) {
|
|
1520
|
+
const match = value.match(/\{([^{}]+)\}/);
|
|
1521
|
+
if (!match) {
|
|
1522
|
+
return [value];
|
|
1523
|
+
}
|
|
1524
|
+
const variants = match[1]
|
|
1525
|
+
.split(',')
|
|
1526
|
+
.map((part) => part.trim())
|
|
1527
|
+
.filter(Boolean);
|
|
1528
|
+
if (!variants.length) {
|
|
1529
|
+
return [value.replace(match[0], '')];
|
|
1530
|
+
}
|
|
1531
|
+
return variants.flatMap((variant) => expandBraceExpression(value.replace(match[0], variant)));
|
|
1532
|
+
}
|
|
1533
|
+
function tokenizeValue(value, context, surface, path) {
|
|
1534
|
+
if (typeof value === 'string') {
|
|
1535
|
+
if (isAlreadyPrivacyToken(value)) {
|
|
1536
|
+
return value;
|
|
1537
|
+
}
|
|
1538
|
+
if (looksLikeEmail(value)) {
|
|
1539
|
+
return (0, privacy_redaction_1.redactEmailDeterministic)(value);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
const namespace = `${surface}:${path.join('.') || '$'}`;
|
|
1543
|
+
const token = stableToken(namespace, value, context);
|
|
1544
|
+
if (typeof value === 'string') {
|
|
1545
|
+
return `${path[path.length - 1] || 'tok'}_tok_${token}`;
|
|
1546
|
+
}
|
|
1547
|
+
if (typeof value === 'number' || typeof value === 'bigint') {
|
|
1548
|
+
return `num_tok_${token}`;
|
|
1549
|
+
}
|
|
1550
|
+
if (typeof value === 'boolean') {
|
|
1551
|
+
return value;
|
|
1552
|
+
}
|
|
1553
|
+
if (Array.isArray(value)) {
|
|
1554
|
+
return value.map(() => '[tokenized]');
|
|
1555
|
+
}
|
|
1556
|
+
if (value && typeof value === 'object') {
|
|
1557
|
+
const output = {};
|
|
1558
|
+
for (const key of Object.keys(value)) {
|
|
1559
|
+
output[key] = '[tokenized]';
|
|
1560
|
+
}
|
|
1561
|
+
return output;
|
|
1562
|
+
}
|
|
1563
|
+
return {
|
|
1564
|
+
type: typeof value,
|
|
1565
|
+
token: `tok_${token}`,
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
function isAlreadyPrivacyToken(value) {
|
|
1569
|
+
return /^(?:email_tok_|[A-Za-z0-9_.-]+_tok_|num_tok_|tok_)[a-f0-9]{8,}$/i.test(value.trim());
|
|
1570
|
+
}
|
|
1571
|
+
const SUMMARY_MAX_DEPTH = 8;
|
|
1572
|
+
const SUMMARY_MAX_KEYS = 40;
|
|
1573
|
+
const SUMMARY_MAX_ITEMS = 10;
|
|
1574
|
+
const EXACT_GLUE_WORDS = new Set([
|
|
1575
|
+
'a',
|
|
1576
|
+
'an',
|
|
1577
|
+
'and',
|
|
1578
|
+
'as',
|
|
1579
|
+
'at',
|
|
1580
|
+
'but',
|
|
1581
|
+
'by',
|
|
1582
|
+
'be',
|
|
1583
|
+
'can',
|
|
1584
|
+
'could',
|
|
1585
|
+
'did',
|
|
1586
|
+
'do',
|
|
1587
|
+
'does',
|
|
1588
|
+
'for',
|
|
1589
|
+
'from',
|
|
1590
|
+
'had',
|
|
1591
|
+
'has',
|
|
1592
|
+
'have',
|
|
1593
|
+
'if',
|
|
1594
|
+
'in',
|
|
1595
|
+
'into',
|
|
1596
|
+
'is',
|
|
1597
|
+
'it',
|
|
1598
|
+
'must',
|
|
1599
|
+
'never',
|
|
1600
|
+
'not',
|
|
1601
|
+
'of',
|
|
1602
|
+
'on',
|
|
1603
|
+
'or',
|
|
1604
|
+
'should',
|
|
1605
|
+
'the',
|
|
1606
|
+
'then',
|
|
1607
|
+
'to',
|
|
1608
|
+
'up',
|
|
1609
|
+
'via',
|
|
1610
|
+
'was',
|
|
1611
|
+
'were',
|
|
1612
|
+
'will',
|
|
1613
|
+
'with',
|
|
1614
|
+
'would',
|
|
1615
|
+
]);
|
|
1616
|
+
const SUMMARY_SAFE_PATH_PATTERNS = [
|
|
1617
|
+
/(^|\.)(_?id|taskid|paymentid|orderid|invoiceid|shipmentid)$/i,
|
|
1618
|
+
/(^|\.)(reference|paymentreference|code|sku)$/i,
|
|
1619
|
+
/(^|\.)(eventid|eventtype)$/i,
|
|
1620
|
+
/(^|\.)(status|taskstatus|rawgatewaystatus|priority|scenario|success|classification)$/i,
|
|
1621
|
+
/(^|\.)(currency|method|source|service|channel|direction|topic|partition|offset)$/i,
|
|
1622
|
+
/(^|\.)(amount|refundedamount|count|total|port)$/i,
|
|
1623
|
+
/(^|\.)(messagekey)$/i,
|
|
1624
|
+
/(^|\.)(timestamp|firsttimestamp|maxtimestamp|logappendtime|createdat|updatedat|processedat|refundedat)$/i,
|
|
1625
|
+
/(^|\.)(tasktitle|title)$/i,
|
|
1626
|
+
/(^|\.)(queue|safequeue|lane|safelane|region|saferegion|stage|workflowstage|x-workflow-stage|featureflag|safefeatureflag|reasoncode|shard)$/i,
|
|
1627
|
+
/(^|\.)(provider|component|environment|step)$/i,
|
|
1628
|
+
];
|
|
1629
|
+
const SUMMARY_UNSAFE_PATH_PATTERNS = [
|
|
1630
|
+
/(^|\.)(description|message|comment|note|statusnote|failurereason|reason|error)$/i,
|
|
1631
|
+
/\bemail\b/i,
|
|
1632
|
+
/\bname\b/i,
|
|
1633
|
+
/\bphone\b/i,
|
|
1634
|
+
/\baddress\b/i,
|
|
1635
|
+
/\b(dob|birthdate|dateofbirth)\b/i,
|
|
1636
|
+
/\btoken\b/i,
|
|
1637
|
+
/\bsecret\b/i,
|
|
1638
|
+
/\bpassword\b/i,
|
|
1639
|
+
/\bconnection\b/i,
|
|
1640
|
+
/\bdsn\b/i,
|
|
1641
|
+
/\buri\b/i,
|
|
1642
|
+
/\burl\b/i,
|
|
1643
|
+
/\bcard(last4|number|expiry|cvv)?\b/i,
|
|
1644
|
+
];
|
|
1645
|
+
const DROP_SAFETY_PATH_PATTERNS = [
|
|
1646
|
+
/(^|\.)(authorization|x[-_]?apikey|x[-_]?api[-_]?key|apikey|api[-_]?key|appsecret|secret|token|password|cookie|sessionid|credential|privatekey|refresh|refresh[-_]?token)$/i,
|
|
1647
|
+
/(^|\.)([a-z0-9_-]*(auth|authorization|bearer)|[a-z0-9_-]*secret|[a-z0-9_-]*token|[a-z0-9_-]*password|[a-z0-9_-]*cookie|[a-z0-9_-]*credential)$/i,
|
|
1648
|
+
/(^|\.)(providerconnection|rawgatewayconnection|connectionstring|connectionuri|connectionurl|dsn|uri)$/i,
|
|
1649
|
+
/(^|\.)([a-z0-9_-]*(connection|dsn))$/i,
|
|
1650
|
+
/(^|\.)(cardlast4|cardnumber|card[-_]?number|fullpan|pan|cvv|cvc|securitycode|expiry)$/i,
|
|
1651
|
+
];
|
|
1652
|
+
const TOKENIZE_SAFETY_PATH_PATTERNS = [
|
|
1653
|
+
/(^|\.)([a-z0-9_-]*email|email)$/i,
|
|
1654
|
+
/(^|\.)(customername|customer[-_]?name|operatorname|operator[-_]?name|username|user[-_]?name|fullname|full[-_]?name|firstname|first[-_]?name|lastname|last[-_]?name)$/i,
|
|
1655
|
+
/(^|\.)(incidentownername|incident[-_]?owner[-_]?name|ownername|owner[-_]?name|displayname|display[-_]?name|contactname|contact[-_]?name|reviewername|reviewer[-_]?name|requestername|requester[-_]?name|approvername|approver[-_]?name|assigneename|assignee[-_]?name|analystname|analyst[-_]?name|agentname|agent[-_]?name|payername|payer[-_]?name|payeename|payee[-_]?name)$/i,
|
|
1656
|
+
/(^|\.)(accountid|account[-_]?id|customerid|customer[-_]?id|userid|user[-_]?id|personid|person[-_]?id|profileid|profile[-_]?id|contactid|contact[-_]?id|externaluserid|external[-_]?user[-_]?id|externalcustomerid|external[-_]?customer[-_]?id)$/i,
|
|
1657
|
+
/(^|\.)(customer|user|account|person|profile|contact|owner|incidentowner|operator|reviewer|requester|approver|assignee|analyst|agent|payer|payee)\.(id|name|displayname|fullname|firstname|lastname)$/i,
|
|
1658
|
+
];
|
|
1659
|
+
const FREE_TEXT_FALLBACK_PATH_PATTERNS = [
|
|
1660
|
+
/(^|\.)(description|message|comment|note|statusnote|failurereason|reason|error)$/i,
|
|
1661
|
+
];
|
|
1662
|
+
const OPERATIONAL_SAFE_PATH_PATTERNS = [
|
|
1663
|
+
/(^|\.)(_?id|taskid|paymentid|orderid|safeorderid|invoiceid|shipmentid|drillid|caseid)$/i,
|
|
1664
|
+
/(^|\.)(reference|paymentreference|messagekey|cachekey|rediskey|key)$/i,
|
|
1665
|
+
/(^|\.)(eventid|requestid|rid|traceid|spanid|parentspanid|correlationid)$/i,
|
|
1666
|
+
/(^|\.)(timestamp|firsttimestamp|maxtimestamp|logappendtime|createdat|updatedat|processedat|occurredat|receivedat|sentat|startedat|endedat|completedat|queuedat|scheduledat|duedate)$/i,
|
|
1667
|
+
/(^|\.)(x-repro-request-rid|x-repro-trace-id|x-bug-request-start|x-request-id|x-correlation-id)$/i,
|
|
1668
|
+
/(^|\.)(status|taskstatus|rawgatewaystatus|classification|currency|method|priority|risklevel|severity|safeseverity|level|scenario)$/i,
|
|
1669
|
+
/(^|\.)(queue|safequeue|lane|safelane|region|saferegion|stage|workflowstage|x-workflow-stage|featureflag|safefeatureflag|reasoncode|shard)$/i,
|
|
1670
|
+
/(^|\.)(provider|component|environment|step)$/i,
|
|
1671
|
+
];
|
|
1672
|
+
const OPERATIONAL_ENUM_PATH_PATTERNS = [
|
|
1673
|
+
/(^|\.)([a-z0-9_-]*(status|state|priority|severity|level|classification|category|scenario|phase|stage|mode|method|currency|queue|lane|region|environment|component|provider|service|source|channel|direction|reasoncode|featureflag|shard))$/i,
|
|
1674
|
+
];
|
|
1675
|
+
const OPERATIONAL_COUNT_PATH_PATTERNS = [
|
|
1676
|
+
/(^|\.)([a-z0-9_-]*(count|total|attempt|attempts|retry|retries|limit|offset|partition|size|index|position))$/i,
|
|
1677
|
+
];
|
|
1678
|
+
const OPERATIONAL_TIMESTAMP_PATH_PATTERNS = [
|
|
1679
|
+
/(^|\.)(timestamp|firsttimestamp|maxtimestamp|logappendtime|[a-z0-9_-]*(created|updated|processed|occurred|received|sent|started|ended|completed|queued|scheduled|due)at|duedate)$/i,
|
|
1680
|
+
];
|
|
1681
|
+
const OPERATIONAL_ID_VALUE_PATTERNS = [
|
|
1682
|
+
/^[0-9a-f]{24}$/i,
|
|
1683
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
1684
|
+
/^S_[0-9a-f-]{16,}$/i,
|
|
1685
|
+
/^A_\d+_[a-z0-9]+$/i,
|
|
1686
|
+
/^\d{10,14}-[a-z0-9]+(?:-[a-z0-9]+)?$/i,
|
|
1687
|
+
/^[A-Z][A-Z0-9]{1,15}-[A-Z0-9][A-Z0-9-]{1,31}$/,
|
|
1688
|
+
/^[a-z][a-z0-9_-]*(?::[a-z0-9_-]+){1,}:[A-Za-z0-9_.-]{4,}$/i,
|
|
1689
|
+
];
|
|
1690
|
+
const SERIALIZED_STRUCTURED_TEXT_MAX_LENGTH = 200000;
|
|
1691
|
+
function summarizeValue(policy, value, context, surface, path, statement) {
|
|
1692
|
+
const depth = path.length;
|
|
1693
|
+
if (typeof value === 'string') {
|
|
1694
|
+
const textPolicy = statement?.textPolicy;
|
|
1695
|
+
if (textPolicy?.mode === 'KNOWN_TEMPLATE') {
|
|
1696
|
+
return applyKnownTextPolicy(textPolicy, value, context, surface, path);
|
|
1697
|
+
}
|
|
1698
|
+
let text = value;
|
|
1699
|
+
if (!statement?.rawTextMode && shouldKeepOperationalStringExact(value, path)) {
|
|
1700
|
+
return value;
|
|
1701
|
+
}
|
|
1702
|
+
if (statement?.rawTextMode === 'REGEX_ASSISTED_EXACT' && policy.rawTextHints.length) {
|
|
1703
|
+
return sanitizeRawTextExactSync(policy, value, context, surface, path, statement.rawTextHintNames);
|
|
1704
|
+
}
|
|
1705
|
+
const detected = sanitizeTextWithKnownDetectorsSync(value, path);
|
|
1706
|
+
if (detected !== value) {
|
|
1707
|
+
text = detected;
|
|
1708
|
+
}
|
|
1709
|
+
if (statement?.rawTextMode === 'SAFE_PARTIAL_MASK') {
|
|
1710
|
+
if (shouldKeepSafePartialMaskStringExact(text, path)) {
|
|
1711
|
+
return text;
|
|
1712
|
+
}
|
|
1713
|
+
return partiallyMaskUnknownString(text);
|
|
1714
|
+
}
|
|
1715
|
+
return text;
|
|
1716
|
+
}
|
|
1717
|
+
if (value === null || value === undefined) {
|
|
1718
|
+
return value;
|
|
1719
|
+
}
|
|
1720
|
+
if (typeof value === 'number' || typeof value === 'bigint') {
|
|
1721
|
+
return value;
|
|
1722
|
+
}
|
|
1723
|
+
if (typeof value === 'boolean') {
|
|
1724
|
+
return value;
|
|
1725
|
+
}
|
|
1726
|
+
if (value instanceof Date) {
|
|
1727
|
+
return value.toISOString();
|
|
1728
|
+
}
|
|
1729
|
+
if (Array.isArray(value)) {
|
|
1730
|
+
const visibleItems = value.slice(0, SUMMARY_MAX_ITEMS).map((item, index) => transformValue(policy, surface, item, context, path.concat(String(index)), 'SUMMARIZE'));
|
|
1731
|
+
if (value.length > SUMMARY_MAX_ITEMS) {
|
|
1732
|
+
visibleItems.push(`[truncated ${value.length - SUMMARY_MAX_ITEMS} items]`);
|
|
1733
|
+
}
|
|
1734
|
+
return visibleItems;
|
|
1735
|
+
}
|
|
1736
|
+
if (value instanceof Map) {
|
|
1737
|
+
const fields = {};
|
|
1738
|
+
let index = 0;
|
|
1739
|
+
for (const [key, item] of value.entries()) {
|
|
1740
|
+
if (index >= SUMMARY_MAX_KEYS)
|
|
1741
|
+
break;
|
|
1742
|
+
const normalizedKey = normalizeCollectionKeyForPrivacy(key, index);
|
|
1743
|
+
fields[normalizedKey] = transformValue(policy, surface, item, context, path.concat(normalizedKey), 'SUMMARIZE');
|
|
1744
|
+
index += 1;
|
|
1745
|
+
}
|
|
1746
|
+
if (value.size > SUMMARY_MAX_KEYS) {
|
|
1747
|
+
fields.__reproTruncatedKeys = value.size - SUMMARY_MAX_KEYS;
|
|
1748
|
+
}
|
|
1749
|
+
return fields;
|
|
1750
|
+
}
|
|
1751
|
+
if (value instanceof Set) {
|
|
1752
|
+
const visibleItems = Array.from(value.values()).slice(0, SUMMARY_MAX_ITEMS).map((item, index) => transformValue(policy, surface, item, context, path.concat(String(index)), 'SUMMARIZE'));
|
|
1753
|
+
if (value.size > SUMMARY_MAX_ITEMS) {
|
|
1754
|
+
visibleItems.push(`[truncated ${value.size - SUMMARY_MAX_ITEMS} items]`);
|
|
1755
|
+
}
|
|
1756
|
+
return visibleItems;
|
|
1757
|
+
}
|
|
1758
|
+
if (isWeakCollection(value)) {
|
|
1759
|
+
return `[${getPrivacyConstructorName(value)}: uninspectable]`;
|
|
1760
|
+
}
|
|
1761
|
+
if (value instanceof Error) {
|
|
1762
|
+
const output = {
|
|
1763
|
+
name: value.name,
|
|
1764
|
+
code: typeof value.code === 'string' || typeof value.code === 'number'
|
|
1765
|
+
? String(value.code)
|
|
1766
|
+
: undefined,
|
|
1767
|
+
status: typeof value.status === 'number'
|
|
1768
|
+
? value.status
|
|
1769
|
+
: typeof value.statusCode === 'number'
|
|
1770
|
+
? value.statusCode
|
|
1771
|
+
: undefined,
|
|
1772
|
+
message: typeof value.message === 'string'
|
|
1773
|
+
? transformValue(policy, surface, value.message, context, path.concat('message'), 'SUMMARIZE')
|
|
1774
|
+
: undefined,
|
|
1775
|
+
};
|
|
1776
|
+
return output;
|
|
1777
|
+
}
|
|
1778
|
+
if (value && typeof value === 'object') {
|
|
1779
|
+
if (depth >= SUMMARY_MAX_DEPTH) {
|
|
1780
|
+
return '[truncated object depth]';
|
|
1781
|
+
}
|
|
1782
|
+
const keys = Object.keys(value);
|
|
1783
|
+
const visibleKeys = keys.slice(0, SUMMARY_MAX_KEYS);
|
|
1784
|
+
const fields = {};
|
|
1785
|
+
for (const key of visibleKeys) {
|
|
1786
|
+
fields[key] = transformValue(policy, surface, value[key], context, path.concat(key), 'SUMMARIZE');
|
|
1787
|
+
}
|
|
1788
|
+
if (keys.length > SUMMARY_MAX_KEYS) {
|
|
1789
|
+
fields.__reproTruncatedKeys = keys.length - SUMMARY_MAX_KEYS;
|
|
1790
|
+
}
|
|
1791
|
+
return fields;
|
|
1792
|
+
}
|
|
1793
|
+
return value;
|
|
1794
|
+
}
|
|
1795
|
+
async function summarizeValueAsync(policy, value, context, surface, path, statement) {
|
|
1796
|
+
const depth = path.length;
|
|
1797
|
+
if (typeof value === 'string') {
|
|
1798
|
+
const textPolicy = statement?.textPolicy;
|
|
1799
|
+
if (textPolicy?.mode === 'KNOWN_TEMPLATE') {
|
|
1800
|
+
return applyKnownTextPolicy(textPolicy, value, context, surface, path);
|
|
1801
|
+
}
|
|
1802
|
+
let text = value;
|
|
1803
|
+
if (!statement?.rawTextMode && shouldKeepOperationalStringExact(value, path)) {
|
|
1804
|
+
return value;
|
|
1805
|
+
}
|
|
1806
|
+
if (statement?.rawTextMode === 'REGEX_ASSISTED_EXACT' && policy.rawTextHints.length) {
|
|
1807
|
+
return sanitizeRawTextExactSync(policy, value, context, surface, path, statement.rawTextHintNames);
|
|
1808
|
+
}
|
|
1809
|
+
const syncDetected = sanitizeTextWithKnownDetectorsSync(value, path);
|
|
1810
|
+
if (syncDetected !== value) {
|
|
1811
|
+
text = syncDetected;
|
|
1812
|
+
}
|
|
1813
|
+
if (statement?.rawTextMode === 'SAFE_PARTIAL_MASK') {
|
|
1814
|
+
const detected = await (0, privacy_fallback_1.sanitizeUnknownBoundaryTextWithDetectors)(text);
|
|
1815
|
+
if (detected !== text) {
|
|
1816
|
+
text = detected;
|
|
1817
|
+
}
|
|
1818
|
+
if (shouldKeepSafePartialMaskStringExact(text, path)) {
|
|
1819
|
+
return text;
|
|
1820
|
+
}
|
|
1821
|
+
return partiallyMaskUnknownString(text);
|
|
1822
|
+
}
|
|
1823
|
+
return text;
|
|
1824
|
+
}
|
|
1825
|
+
if (value === null || value === undefined) {
|
|
1826
|
+
return value;
|
|
1827
|
+
}
|
|
1828
|
+
if (typeof value === 'number' || typeof value === 'bigint') {
|
|
1829
|
+
return value;
|
|
1830
|
+
}
|
|
1831
|
+
if (typeof value === 'boolean') {
|
|
1832
|
+
return value;
|
|
1833
|
+
}
|
|
1834
|
+
if (value instanceof Date) {
|
|
1835
|
+
return value.toISOString();
|
|
1836
|
+
}
|
|
1837
|
+
if (Array.isArray(value)) {
|
|
1838
|
+
const visibleItems = await Promise.all(value.slice(0, SUMMARY_MAX_ITEMS).map((item, index) => transformValueAsync(policy, surface, item, context, path.concat(String(index)), 'SUMMARIZE')));
|
|
1839
|
+
if (value.length > SUMMARY_MAX_ITEMS) {
|
|
1840
|
+
visibleItems.push(`[truncated ${value.length - SUMMARY_MAX_ITEMS} items]`);
|
|
1841
|
+
}
|
|
1842
|
+
return visibleItems;
|
|
1843
|
+
}
|
|
1844
|
+
if (value instanceof Map) {
|
|
1845
|
+
const fields = {};
|
|
1846
|
+
let index = 0;
|
|
1847
|
+
for (const [key, item] of value.entries()) {
|
|
1848
|
+
if (index >= SUMMARY_MAX_KEYS)
|
|
1849
|
+
break;
|
|
1850
|
+
const normalizedKey = normalizeCollectionKeyForPrivacy(key, index);
|
|
1851
|
+
fields[normalizedKey] = await transformValueAsync(policy, surface, item, context, path.concat(normalizedKey), 'SUMMARIZE');
|
|
1852
|
+
index += 1;
|
|
1853
|
+
}
|
|
1854
|
+
if (value.size > SUMMARY_MAX_KEYS) {
|
|
1855
|
+
fields.__reproTruncatedKeys = value.size - SUMMARY_MAX_KEYS;
|
|
1856
|
+
}
|
|
1857
|
+
return fields;
|
|
1858
|
+
}
|
|
1859
|
+
if (value instanceof Set) {
|
|
1860
|
+
const visibleItems = await Promise.all(Array.from(value.values()).slice(0, SUMMARY_MAX_ITEMS).map((item, index) => transformValueAsync(policy, surface, item, context, path.concat(String(index)), 'SUMMARIZE')));
|
|
1861
|
+
if (value.size > SUMMARY_MAX_ITEMS) {
|
|
1862
|
+
visibleItems.push(`[truncated ${value.size - SUMMARY_MAX_ITEMS} items]`);
|
|
1863
|
+
}
|
|
1864
|
+
return visibleItems;
|
|
1865
|
+
}
|
|
1866
|
+
if (isWeakCollection(value)) {
|
|
1867
|
+
return `[${getPrivacyConstructorName(value)}: uninspectable]`;
|
|
1868
|
+
}
|
|
1869
|
+
if (value instanceof Error) {
|
|
1870
|
+
const output = {
|
|
1871
|
+
name: value.name,
|
|
1872
|
+
code: typeof value.code === 'string' || typeof value.code === 'number'
|
|
1873
|
+
? String(value.code)
|
|
1874
|
+
: undefined,
|
|
1875
|
+
status: typeof value.status === 'number'
|
|
1876
|
+
? value.status
|
|
1877
|
+
: typeof value.statusCode === 'number'
|
|
1878
|
+
? value.statusCode
|
|
1879
|
+
: undefined,
|
|
1880
|
+
message: typeof value.message === 'string'
|
|
1881
|
+
? await transformValueAsync(policy, surface, value.message, context, path.concat('message'), 'SUMMARIZE')
|
|
1882
|
+
: undefined,
|
|
1883
|
+
};
|
|
1884
|
+
return output;
|
|
1885
|
+
}
|
|
1886
|
+
if (value && typeof value === 'object') {
|
|
1887
|
+
if (depth >= SUMMARY_MAX_DEPTH) {
|
|
1888
|
+
return '[truncated object depth]';
|
|
1889
|
+
}
|
|
1890
|
+
const keys = Object.keys(value);
|
|
1891
|
+
const visibleKeys = keys.slice(0, SUMMARY_MAX_KEYS);
|
|
1892
|
+
const fields = {};
|
|
1893
|
+
for (const key of visibleKeys) {
|
|
1894
|
+
fields[key] = await transformValueAsync(policy, surface, value[key], context, path.concat(key), 'SUMMARIZE');
|
|
1895
|
+
}
|
|
1896
|
+
if (keys.length > SUMMARY_MAX_KEYS) {
|
|
1897
|
+
fields.__reproTruncatedKeys = keys.length - SUMMARY_MAX_KEYS;
|
|
1898
|
+
}
|
|
1899
|
+
return fields;
|
|
1900
|
+
}
|
|
1901
|
+
return value;
|
|
1902
|
+
}
|
|
1903
|
+
function applyKnownTextPolicy(policy, value, context, surface, path) {
|
|
1904
|
+
if (policy.mode !== 'KNOWN_TEMPLATE' || !policy.fragments.length) {
|
|
1905
|
+
return sanitizeTextWithKnownDetectorsSync(value, path);
|
|
1906
|
+
}
|
|
1907
|
+
let cursor = 0;
|
|
1908
|
+
let output = '';
|
|
1909
|
+
for (let index = 0; index < policy.fragments.length; index += 1) {
|
|
1910
|
+
const fragment = policy.fragments[index];
|
|
1911
|
+
if (fragment.kind === 'literal') {
|
|
1912
|
+
if (!value.startsWith(fragment.text, cursor)) {
|
|
1913
|
+
return sanitizeTextWithKnownDetectorsSync(value, path);
|
|
1914
|
+
}
|
|
1915
|
+
output += fragment.text;
|
|
1916
|
+
cursor += fragment.text.length;
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
const nextLiteral = findNextLiteral(policy.fragments, index + 1);
|
|
1920
|
+
const end = nextLiteral ? value.indexOf(nextLiteral.text, cursor) : value.length;
|
|
1921
|
+
if (end < 0) {
|
|
1922
|
+
return sanitizeTextWithKnownDetectorsSync(value, path);
|
|
1923
|
+
}
|
|
1924
|
+
const raw = value.slice(cursor, end);
|
|
1925
|
+
output += applyTextSlotAction(fragment, raw, context, surface, path, index);
|
|
1926
|
+
cursor = end;
|
|
1927
|
+
}
|
|
1928
|
+
return cursor === value.length ? normalizePrivacyReplacementSpacing(output) : sanitizeTextWithKnownDetectorsSync(value, path);
|
|
1929
|
+
}
|
|
1930
|
+
function applyTextSlotAction(fragment, raw, context, surface, path, index) {
|
|
1931
|
+
switch (fragment.action) {
|
|
1932
|
+
case 'KEEP_EXACT':
|
|
1933
|
+
return keepExactExceptKnownSensitiveText(raw);
|
|
1934
|
+
case 'TOKENIZE':
|
|
1935
|
+
return String(tokenizeValue(raw, context, surface, path.concat(fragment.label ?? `slot${index}`)));
|
|
1936
|
+
case 'DROP':
|
|
1937
|
+
return '[dropped]';
|
|
1938
|
+
case 'PARTIAL_MASK':
|
|
1939
|
+
default:
|
|
1940
|
+
return partiallyMaskUnknownString(raw);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
function keepExactExceptKnownSensitiveText(value) {
|
|
1944
|
+
const spans = detectSensitiveTextSpans(value);
|
|
1945
|
+
if (!spans.length) {
|
|
1946
|
+
return value;
|
|
1947
|
+
}
|
|
1948
|
+
let cursor = 0;
|
|
1949
|
+
let output = '';
|
|
1950
|
+
for (const span of spans) {
|
|
1951
|
+
if (span.start > cursor) {
|
|
1952
|
+
output += value.slice(cursor, span.start);
|
|
1953
|
+
}
|
|
1954
|
+
output += span.replacement;
|
|
1955
|
+
cursor = span.end;
|
|
1956
|
+
}
|
|
1957
|
+
if (cursor < value.length) {
|
|
1958
|
+
output += value.slice(cursor);
|
|
1959
|
+
}
|
|
1960
|
+
return normalizePrivacyReplacementSpacing(output);
|
|
1961
|
+
}
|
|
1962
|
+
function sanitizeRawTextExactSync(policy, value, context, surface, path, rawTextHintNames) {
|
|
1963
|
+
const hinted = applyRawTextHints(value, selectRawTextHints(policy.rawTextHints, rawTextHintNames), context, surface, path);
|
|
1964
|
+
return sanitizeTextWithKnownDetectorsSync(hinted, path);
|
|
1965
|
+
}
|
|
1966
|
+
function selectRawTextHints(hints, rawTextHintNames) {
|
|
1967
|
+
if (!rawTextHintNames?.length) {
|
|
1968
|
+
return hints;
|
|
1969
|
+
}
|
|
1970
|
+
const allowed = new Set(rawTextHintNames.map((name) => name.trim()).filter(Boolean));
|
|
1971
|
+
const selected = hints.filter((hint) => allowed.has(hint.name));
|
|
1972
|
+
return selected.length ? selected : hints;
|
|
1973
|
+
}
|
|
1974
|
+
function applyRawTextHints(value, hints, context, surface, path) {
|
|
1975
|
+
if (!value || !hints.length) {
|
|
1976
|
+
return normalizePrivacyReplacementSpacing(value);
|
|
1977
|
+
}
|
|
1978
|
+
const spans = [];
|
|
1979
|
+
for (const hint of sortRawTextHintsForPath(hints, path)) {
|
|
1980
|
+
collectRawTextHintSpans(value, hint, context, surface, path, spans);
|
|
1981
|
+
}
|
|
1982
|
+
if (!spans.length) {
|
|
1983
|
+
return normalizePrivacyReplacementSpacing(value);
|
|
1984
|
+
}
|
|
1985
|
+
let cursor = 0;
|
|
1986
|
+
let output = '';
|
|
1987
|
+
for (const span of mergeSpans(spans)) {
|
|
1988
|
+
if (span.start > cursor) {
|
|
1989
|
+
output += value.slice(cursor, span.start);
|
|
1990
|
+
}
|
|
1991
|
+
output += span.replacement;
|
|
1992
|
+
cursor = span.end;
|
|
1993
|
+
}
|
|
1994
|
+
if (cursor < value.length) {
|
|
1995
|
+
output += value.slice(cursor);
|
|
1996
|
+
}
|
|
1997
|
+
return normalizePrivacyReplacementSpacing(output);
|
|
1998
|
+
}
|
|
1999
|
+
function sortRawTextHintsForPath(hints, path) {
|
|
2000
|
+
const normalizedPath = normalizeRawTextHintNeedle(path.join('.'));
|
|
2001
|
+
return hints.slice().sort((left, right) => rawTextHintPathScore(left, normalizedPath) - rawTextHintPathScore(right, normalizedPath));
|
|
2002
|
+
}
|
|
2003
|
+
function rawTextHintPathScore(hint, normalizedPath) {
|
|
2004
|
+
const tokenLabel = normalizeRawTextHintNeedle(hint.tokenLabel);
|
|
2005
|
+
const name = normalizeRawTextHintNeedle(hint.name);
|
|
2006
|
+
if (tokenLabel && normalizedPath.includes(tokenLabel))
|
|
2007
|
+
return 0;
|
|
2008
|
+
if (name && normalizedPath.includes(name))
|
|
2009
|
+
return 1;
|
|
2010
|
+
return 2;
|
|
2011
|
+
}
|
|
2012
|
+
function normalizeRawTextHintNeedle(value) {
|
|
2013
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
2014
|
+
}
|
|
2015
|
+
function collectRawTextHintSpans(value, hint, context, surface, path, out) {
|
|
2016
|
+
const pattern = new RegExp(hint.pattern.source, hint.pattern.flags.includes('g') ? hint.pattern.flags : `${hint.pattern.flags}g`);
|
|
2017
|
+
let match;
|
|
2018
|
+
let guard = 0;
|
|
2019
|
+
while ((match = pattern.exec(value))) {
|
|
2020
|
+
const matched = match[0];
|
|
2021
|
+
if (!matched) {
|
|
2022
|
+
pattern.lastIndex += 1;
|
|
2023
|
+
if (pattern.lastIndex > value.length || guard++ > 1000) {
|
|
2024
|
+
break;
|
|
2025
|
+
}
|
|
2026
|
+
continue;
|
|
2027
|
+
}
|
|
2028
|
+
out.push({
|
|
2029
|
+
start: match.index,
|
|
2030
|
+
end: match.index + matched.length,
|
|
2031
|
+
replacement: hint.action === 'DROP'
|
|
2032
|
+
? '[dropped]'
|
|
2033
|
+
: String(tokenizeValue(matched, context, surface, path.concat(hint.tokenLabel))),
|
|
2034
|
+
});
|
|
2035
|
+
guard += 1;
|
|
2036
|
+
if (guard > 1000) {
|
|
2037
|
+
break;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
function findNextLiteral(fragments, startIndex) {
|
|
2042
|
+
for (let index = startIndex; index < fragments.length; index += 1) {
|
|
2043
|
+
const fragment = fragments[index];
|
|
2044
|
+
if (fragment.kind === 'literal') {
|
|
2045
|
+
return fragment;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
return null;
|
|
2049
|
+
}
|
|
2050
|
+
function partiallyMaskUnknownString(value) {
|
|
2051
|
+
if (!value) {
|
|
2052
|
+
return value;
|
|
2053
|
+
}
|
|
2054
|
+
const spans = detectSensitiveTextSpans(value);
|
|
2055
|
+
if (!spans.length) {
|
|
2056
|
+
return maskGeneralTextSegment(value);
|
|
2057
|
+
}
|
|
2058
|
+
let cursor = 0;
|
|
2059
|
+
let output = '';
|
|
2060
|
+
for (const span of spans) {
|
|
2061
|
+
if (span.start > cursor) {
|
|
2062
|
+
output += maskGeneralTextSegment(value.slice(cursor, span.start));
|
|
2063
|
+
}
|
|
2064
|
+
output += span.replacement;
|
|
2065
|
+
cursor = span.end;
|
|
2066
|
+
}
|
|
2067
|
+
if (cursor < value.length) {
|
|
2068
|
+
output += maskGeneralTextSegment(value.slice(cursor));
|
|
2069
|
+
}
|
|
2070
|
+
return normalizePrivacyReplacementSpacing(output);
|
|
2071
|
+
}
|
|
2072
|
+
function normalizePrivacyReplacementSpacing(value) {
|
|
2073
|
+
if (!value) {
|
|
2074
|
+
return value;
|
|
2075
|
+
}
|
|
2076
|
+
const bracketMarker = String.raw `(?:\[(?:dropped|secret-token|connection-string|auth-token|session-secret|private-key|basic-auth|jwt|credit-card|phone|person|ssn|masked)\])`;
|
|
2077
|
+
const tokenMarker = String.raw `(?:email_tok_[a-f0-9]{8,}|[A-Za-z][A-Za-z0-9_.-]*_tok_[a-f0-9]{8,})`;
|
|
2078
|
+
const glueWord = String.raw `(?:for|from|by|to|with|owner|user|customer|operator|account|email|name)`;
|
|
2079
|
+
const suffixGlueWord = String.raw `(?:and|or|on|for|from|to|with|can|was|were|is|in|by)`;
|
|
2080
|
+
return value
|
|
2081
|
+
.replace(new RegExp(`([A-Za-z0-9])(${bracketMarker})`, 'g'), '$1 $2')
|
|
2082
|
+
.replace(new RegExp(`(${bracketMarker})([A-Za-z0-9])`, 'g'), '$1 $2')
|
|
2083
|
+
.replace(new RegExp(`\\b(${glueWord})(${tokenMarker})`, 'gi'), '$1 $2')
|
|
2084
|
+
.replace(new RegExp(`(${tokenMarker})(${suffixGlueWord})\\b`, 'gi'), '$1 $2');
|
|
2085
|
+
}
|
|
2086
|
+
function shouldRunAsyncStringDetector(statement, fallback, value, path) {
|
|
2087
|
+
if (!value || value.length < 8) {
|
|
2088
|
+
return false;
|
|
2089
|
+
}
|
|
2090
|
+
return mergeRawTextMode(statement?.rawTextMode, fallback?.rawTextMode) === 'SAFE_PARTIAL_MASK';
|
|
2091
|
+
}
|
|
2092
|
+
function hasPotentialSensitiveLeak(value) {
|
|
2093
|
+
if (!value) {
|
|
2094
|
+
return false;
|
|
2095
|
+
}
|
|
2096
|
+
if (looksLikeEmail(value) ||
|
|
2097
|
+
looksLikeConnectionString(value) ||
|
|
2098
|
+
containsJwtLikeText(value) ||
|
|
2099
|
+
containsBearerAuthText(value) ||
|
|
2100
|
+
containsBasicAuthText(value) ||
|
|
2101
|
+
containsPrivateKeyBlock(value)) {
|
|
2102
|
+
return true;
|
|
2103
|
+
}
|
|
2104
|
+
if (containsCreditCardLikeText(value)) {
|
|
2105
|
+
return true;
|
|
2106
|
+
}
|
|
2107
|
+
return false;
|
|
2108
|
+
}
|
|
2109
|
+
function shouldKeepOperationalStringExact(value, path = []) {
|
|
2110
|
+
const trimmed = value.trim();
|
|
2111
|
+
if (!trimmed) {
|
|
2112
|
+
return true;
|
|
2113
|
+
}
|
|
2114
|
+
const comparablePath = normalizeComparablePath(path);
|
|
2115
|
+
if (comparablePath && SUMMARY_UNSAFE_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
2116
|
+
return false;
|
|
2117
|
+
}
|
|
2118
|
+
if (comparablePath && DROP_SAFETY_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
2119
|
+
return false;
|
|
2120
|
+
}
|
|
2121
|
+
if (comparablePath && OPERATIONAL_SAFE_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
2122
|
+
return true;
|
|
2123
|
+
}
|
|
2124
|
+
if (comparablePath && OPERATIONAL_TIMESTAMP_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
2125
|
+
return looksLikeOperationalTimestamp(trimmed);
|
|
2126
|
+
}
|
|
2127
|
+
if (comparablePath && OPERATIONAL_COUNT_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
2128
|
+
return isOperationalNumericScalar(trimmed);
|
|
2129
|
+
}
|
|
2130
|
+
if (comparablePath && OPERATIONAL_ENUM_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
2131
|
+
return isLowRiskOperationalScalar(trimmed);
|
|
2132
|
+
}
|
|
2133
|
+
if (containsObviousSensitiveText(trimmed)) {
|
|
2134
|
+
return false;
|
|
2135
|
+
}
|
|
2136
|
+
return OPERATIONAL_ID_VALUE_PATTERNS.some((pattern) => pattern.test(trimmed));
|
|
2137
|
+
}
|
|
2138
|
+
function containsObviousSensitiveText(value) {
|
|
2139
|
+
if (looksLikeEmail(value) ||
|
|
2140
|
+
looksLikeConnectionString(value) ||
|
|
2141
|
+
containsJwtLikeText(value) ||
|
|
2142
|
+
containsBearerAuthText(value) ||
|
|
2143
|
+
containsBasicAuthText(value) ||
|
|
2144
|
+
containsPrivateKeyBlock(value)) {
|
|
2145
|
+
return true;
|
|
2146
|
+
}
|
|
2147
|
+
if (/@/.test(value) || /:\/\/\S+/.test(value)) {
|
|
2148
|
+
return true;
|
|
2149
|
+
}
|
|
2150
|
+
if (containsCreditCardLikeText(value)) {
|
|
2151
|
+
return true;
|
|
2152
|
+
}
|
|
2153
|
+
return false;
|
|
2154
|
+
}
|
|
2155
|
+
function shouldKeepExactUnknownSummaryString(value, path) {
|
|
2156
|
+
if (!value) {
|
|
2157
|
+
return true;
|
|
2158
|
+
}
|
|
2159
|
+
if (shouldKeepOperationalStringExact(value, path)) {
|
|
2160
|
+
return true;
|
|
2161
|
+
}
|
|
2162
|
+
const comparablePath = normalizeComparablePath(path);
|
|
2163
|
+
if (!comparablePath) {
|
|
2164
|
+
return false;
|
|
2165
|
+
}
|
|
2166
|
+
if (SUMMARY_UNSAFE_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
2167
|
+
return false;
|
|
2168
|
+
}
|
|
2169
|
+
if (SUMMARY_SAFE_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
2170
|
+
return true;
|
|
2171
|
+
}
|
|
2172
|
+
return false;
|
|
2173
|
+
}
|
|
2174
|
+
function shouldKeepSafePartialMaskStringExact(value, path) {
|
|
2175
|
+
const trimmed = value.trim();
|
|
2176
|
+
if (!trimmed) {
|
|
2177
|
+
return true;
|
|
2178
|
+
}
|
|
2179
|
+
if (shouldKeepOperationalStringExact(value, path)) {
|
|
2180
|
+
return true;
|
|
2181
|
+
}
|
|
2182
|
+
const comparablePath = normalizeComparablePath(path);
|
|
2183
|
+
if (!comparablePath) {
|
|
2184
|
+
return false;
|
|
2185
|
+
}
|
|
2186
|
+
if (SUMMARY_UNSAFE_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath)) ||
|
|
2187
|
+
DROP_SAFETY_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath)) ||
|
|
2188
|
+
TOKENIZE_SAFETY_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath)) ||
|
|
2189
|
+
containsObviousSensitiveText(trimmed)) {
|
|
2190
|
+
return false;
|
|
2191
|
+
}
|
|
2192
|
+
if (OPERATIONAL_TIMESTAMP_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
2193
|
+
return looksLikeOperationalTimestamp(trimmed) || isLowRiskOperationalScalar(trimmed);
|
|
2194
|
+
}
|
|
2195
|
+
if (OPERATIONAL_COUNT_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
2196
|
+
return isOperationalNumericScalar(trimmed);
|
|
2197
|
+
}
|
|
2198
|
+
if (OPERATIONAL_ENUM_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath))) {
|
|
2199
|
+
return isLowRiskOperationalScalar(trimmed);
|
|
2200
|
+
}
|
|
2201
|
+
return false;
|
|
2202
|
+
}
|
|
2203
|
+
function isLowRiskOperationalScalar(value) {
|
|
2204
|
+
if (!value || value.length > 96) {
|
|
2205
|
+
return false;
|
|
2206
|
+
}
|
|
2207
|
+
if (/\s/.test(value)) {
|
|
2208
|
+
return false;
|
|
2209
|
+
}
|
|
2210
|
+
if (containsObviousSensitiveText(value)) {
|
|
2211
|
+
return false;
|
|
2212
|
+
}
|
|
2213
|
+
return /^[A-Za-z0-9_.:/-]+$/.test(value);
|
|
2214
|
+
}
|
|
2215
|
+
function isOperationalNumericScalar(value) {
|
|
2216
|
+
return /^[+-]?\d+(?:\.\d+)?$/.test(value.trim());
|
|
2217
|
+
}
|
|
2218
|
+
function looksLikeOperationalTimestamp(value) {
|
|
2219
|
+
const trimmed = value.trim();
|
|
2220
|
+
return (/^\d{4}-\d{2}-\d{2}(?:[T ][0-2]\d:[0-5]\d(?::[0-5]\d(?:\.\d{1,9})?)?(?:Z|[+-][0-2]\d:?[0-5]\d)?)?$/.test(trimmed) ||
|
|
2221
|
+
/^\d{10,14}$/.test(trimmed));
|
|
2222
|
+
}
|
|
2223
|
+
function isSummaryUnsafePath(path) {
|
|
2224
|
+
const comparablePath = normalizeComparablePath(path);
|
|
2225
|
+
return Boolean(comparablePath &&
|
|
2226
|
+
SUMMARY_UNSAFE_PATH_PATTERNS.some((pattern) => pattern.test(comparablePath)));
|
|
2227
|
+
}
|
|
2228
|
+
function sanitizeTextWithKnownDetectorsSync(value, path = []) {
|
|
2229
|
+
if (shouldKeepOperationalStringExact(value, path)) {
|
|
2230
|
+
return value;
|
|
2231
|
+
}
|
|
2232
|
+
const structured = sanitizeSerializedStructuredText(value, path);
|
|
2233
|
+
if (structured !== null) {
|
|
2234
|
+
return structured;
|
|
2235
|
+
}
|
|
2236
|
+
let output = value;
|
|
2237
|
+
output = (0, privacy_redaction_1.redactEmailsInText)(output);
|
|
2238
|
+
output = replaceCreditCardCandidates(output, () => '[credit-card]');
|
|
2239
|
+
output = replaceJwtLikeText(output);
|
|
2240
|
+
output = replaceBearerAuthText(output);
|
|
2241
|
+
output = replaceBasicAuthText(output);
|
|
2242
|
+
output = replacePrivateKeyBlocks(output);
|
|
2243
|
+
output = output.replace(/\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^\s"']+/gi, '[connection-string]');
|
|
2244
|
+
return normalizePrivacyReplacementSpacing(output);
|
|
2245
|
+
}
|
|
2246
|
+
function sanitizeSerializedStructuredText(value, path = []) {
|
|
2247
|
+
const parsed = parseSerializedStructuredText(value);
|
|
2248
|
+
if (parsed === null) {
|
|
2249
|
+
return null;
|
|
2250
|
+
}
|
|
2251
|
+
try {
|
|
2252
|
+
const sanitized = sanitizeStructuredValueWithKnownDetectors(parsed, path);
|
|
2253
|
+
if (!sanitized.changed) {
|
|
2254
|
+
return null;
|
|
2255
|
+
}
|
|
2256
|
+
return JSON.stringify(sanitized.value);
|
|
2257
|
+
}
|
|
2258
|
+
catch {
|
|
2259
|
+
return null;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
function parseSerializedStructuredText(value) {
|
|
2263
|
+
if (value.length > SERIALIZED_STRUCTURED_TEXT_MAX_LENGTH) {
|
|
2264
|
+
return null;
|
|
2265
|
+
}
|
|
2266
|
+
const trimmed = value.trim();
|
|
2267
|
+
if (!trimmed || (trimmed[0] !== '{' && trimmed[0] !== '[')) {
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
2270
|
+
try {
|
|
2271
|
+
const parsed = JSON.parse(trimmed);
|
|
2272
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
2273
|
+
return null;
|
|
2274
|
+
}
|
|
2275
|
+
return parsed;
|
|
2276
|
+
}
|
|
2277
|
+
catch {
|
|
2278
|
+
return null;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
function sanitizeStructuredValueWithKnownDetectors(value, path) {
|
|
2282
|
+
const objectId = coercePrivacyObjectId(value);
|
|
2283
|
+
if (objectId !== null) {
|
|
2284
|
+
return { value: objectId, changed: true };
|
|
2285
|
+
}
|
|
2286
|
+
if (Array.isArray(value)) {
|
|
2287
|
+
let changed = false;
|
|
2288
|
+
const next = value.map((item, index) => {
|
|
2289
|
+
const sanitized = sanitizeStructuredValueWithKnownDetectors(item, path.concat(String(index)));
|
|
2290
|
+
changed = changed || sanitized.changed;
|
|
2291
|
+
return sanitized.value;
|
|
2292
|
+
});
|
|
2293
|
+
return { value: next, changed };
|
|
2294
|
+
}
|
|
2295
|
+
if (value && typeof value === 'object') {
|
|
2296
|
+
let changed = false;
|
|
2297
|
+
const next = {};
|
|
2298
|
+
for (const [key, child] of Object.entries(value)) {
|
|
2299
|
+
const sanitized = sanitizeStructuredValueWithKnownDetectors(child, path.concat(key));
|
|
2300
|
+
changed = changed || sanitized.changed;
|
|
2301
|
+
next[key] = sanitized.value;
|
|
2302
|
+
}
|
|
2303
|
+
return { value: next, changed };
|
|
2304
|
+
}
|
|
2305
|
+
const floorAction = deriveSensitiveLeafFloorAction(value, path);
|
|
2306
|
+
if (floorAction === 'DROP') {
|
|
2307
|
+
return { value: '[dropped]', changed: true };
|
|
2308
|
+
}
|
|
2309
|
+
if (floorAction === 'TOKENIZE') {
|
|
2310
|
+
if (typeof value === 'string' && isAlreadyPrivacyToken(value)) {
|
|
2311
|
+
return { value, changed: false };
|
|
2312
|
+
}
|
|
2313
|
+
if (typeof value === 'string' && looksLikeEmail(value)) {
|
|
2314
|
+
return { value: (0, privacy_redaction_1.redactEmailDeterministic)(value), changed: true };
|
|
2315
|
+
}
|
|
2316
|
+
const leaf = path[path.length - 1] || 'value';
|
|
2317
|
+
const token = (0, crypto_1.createHash)('sha256')
|
|
2318
|
+
.update(`repro|${normalizeComparablePath(path)}|${stableStringify(value)}`)
|
|
2319
|
+
.digest('hex')
|
|
2320
|
+
.slice(0, 12);
|
|
2321
|
+
return { value: `${leaf}_tok_${token}`, changed: true };
|
|
2322
|
+
}
|
|
2323
|
+
if (typeof value === 'string') {
|
|
2324
|
+
const sanitized = sanitizeTextWithKnownDetectorsSync(value, path);
|
|
2325
|
+
if (sanitized !== value) {
|
|
2326
|
+
return { value: sanitized, changed: true };
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
return { value, changed: false };
|
|
2330
|
+
}
|
|
2331
|
+
function looksLikeSentenceText(value) {
|
|
2332
|
+
const trimmed = value.trim();
|
|
2333
|
+
if (trimmed.length < 12) {
|
|
2334
|
+
return false;
|
|
2335
|
+
}
|
|
2336
|
+
if (!/\s/.test(trimmed)) {
|
|
2337
|
+
return false;
|
|
2338
|
+
}
|
|
2339
|
+
if (/^[A-Za-z0-9_-]+$/.test(trimmed)) {
|
|
2340
|
+
return false;
|
|
2341
|
+
}
|
|
2342
|
+
return /[A-Za-z]/.test(trimmed);
|
|
2343
|
+
}
|
|
2344
|
+
function maskGeneralTextSegment(value) {
|
|
2345
|
+
const placeholderPattern = /\[[a-z-]+\]/gi;
|
|
2346
|
+
let cursor = 0;
|
|
2347
|
+
let output = '';
|
|
2348
|
+
let match;
|
|
2349
|
+
while ((match = placeholderPattern.exec(value))) {
|
|
2350
|
+
if (match.index > cursor) {
|
|
2351
|
+
output += maskPlainTextTokens(value.slice(cursor, match.index));
|
|
2352
|
+
}
|
|
2353
|
+
output += match[0];
|
|
2354
|
+
cursor = match.index + match[0].length;
|
|
2355
|
+
}
|
|
2356
|
+
if (cursor < value.length) {
|
|
2357
|
+
output += maskPlainTextTokens(value.slice(cursor));
|
|
2358
|
+
}
|
|
2359
|
+
return output;
|
|
2360
|
+
}
|
|
2361
|
+
function maskPlainTextTokens(value) {
|
|
2362
|
+
return value.replace(/[A-Za-z0-9]+(?:[._:/-][A-Za-z0-9]+)*/g, (token) => partiallyMaskToken(token));
|
|
2363
|
+
}
|
|
2364
|
+
function partiallyMaskToken(token) {
|
|
2365
|
+
const length = token.length;
|
|
2366
|
+
if (isAlreadyPrivacyToken(token)) {
|
|
2367
|
+
return token;
|
|
2368
|
+
}
|
|
2369
|
+
if (shouldKeepOperationalStringExact(token)) {
|
|
2370
|
+
return token;
|
|
2371
|
+
}
|
|
2372
|
+
if (EXACT_GLUE_WORDS.has(token.toLowerCase())) {
|
|
2373
|
+
return token;
|
|
2374
|
+
}
|
|
2375
|
+
if (length <= 3) {
|
|
2376
|
+
return '...';
|
|
2377
|
+
}
|
|
2378
|
+
if (/^[A-Za-z]+$/.test(token)) {
|
|
2379
|
+
return maskTokenMiddle(token, alphabeticMaskBounds(length));
|
|
2380
|
+
}
|
|
2381
|
+
return maskTokenMiddle(token, mixedMaskBounds(length));
|
|
2382
|
+
}
|
|
2383
|
+
function alphabeticMaskBounds(length) {
|
|
2384
|
+
if (length <= 4) {
|
|
2385
|
+
return { prefixLength: 1, suffixLength: 1 };
|
|
2386
|
+
}
|
|
2387
|
+
if (length <= 6) {
|
|
2388
|
+
return { prefixLength: 2, suffixLength: 2 };
|
|
2389
|
+
}
|
|
2390
|
+
if (length <= 8) {
|
|
2391
|
+
return { prefixLength: 3, suffixLength: 2 };
|
|
2392
|
+
}
|
|
2393
|
+
return { prefixLength: 3, suffixLength: 3 };
|
|
2394
|
+
}
|
|
2395
|
+
function mixedMaskBounds(length) {
|
|
2396
|
+
if (length <= 5) {
|
|
2397
|
+
return { prefixLength: 1, suffixLength: 1 };
|
|
2398
|
+
}
|
|
2399
|
+
if (length <= 8) {
|
|
2400
|
+
return { prefixLength: 2, suffixLength: 2 };
|
|
2401
|
+
}
|
|
2402
|
+
return { prefixLength: 3, suffixLength: 3 };
|
|
2403
|
+
}
|
|
2404
|
+
function maskTokenMiddle(token, bounds) {
|
|
2405
|
+
const prefixLength = Math.min(bounds.prefixLength, token.length);
|
|
2406
|
+
const suffixLength = Math.min(bounds.suffixLength, Math.max(0, token.length - prefixLength - 1));
|
|
2407
|
+
if (prefixLength + suffixLength >= token.length) {
|
|
2408
|
+
return `${token.slice(0, 1)}..${token.slice(-1)}`;
|
|
2409
|
+
}
|
|
2410
|
+
return `${token.slice(0, prefixLength)}..${token.slice(token.length - suffixLength)}`;
|
|
2411
|
+
}
|
|
2412
|
+
function detectSensitiveTextSpans(value) {
|
|
2413
|
+
const spans = [];
|
|
2414
|
+
collectPatternSpans(value, /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, (match) => maskEmail(match), spans);
|
|
2415
|
+
collectCreditCardSpans(value, spans);
|
|
2416
|
+
return mergeSpans(spans);
|
|
2417
|
+
}
|
|
2418
|
+
function collectPatternSpans(value, pattern, replacer, out) {
|
|
2419
|
+
let match;
|
|
2420
|
+
const globalPattern = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`);
|
|
2421
|
+
while ((match = globalPattern.exec(value))) {
|
|
2422
|
+
const matched = match[0];
|
|
2423
|
+
if (!matched) {
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2426
|
+
out.push({
|
|
2427
|
+
start: match.index,
|
|
2428
|
+
end: match.index + matched.length,
|
|
2429
|
+
replacement: replacer(matched),
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
function collectCreditCardSpans(value, out) {
|
|
2434
|
+
const pattern = /\b(?:\d[ -]*?){13,19}\b/g;
|
|
2435
|
+
let match;
|
|
2436
|
+
while ((match = pattern.exec(value))) {
|
|
2437
|
+
const matched = match[0];
|
|
2438
|
+
if (!matched ||
|
|
2439
|
+
shouldPreserveNumericCandidateInText(value, match.index, matched) ||
|
|
2440
|
+
!isLuhnValidCardCandidate(matched)) {
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
out.push({
|
|
2444
|
+
start: match.index,
|
|
2445
|
+
end: match.index + matched.length,
|
|
2446
|
+
replacement: maskCardLikeValue(matched),
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
function containsCreditCardLikeText(value) {
|
|
2451
|
+
const pattern = /\b(?:\d[ -]*?){13,19}\b/g;
|
|
2452
|
+
let match;
|
|
2453
|
+
while ((match = pattern.exec(value))) {
|
|
2454
|
+
if (match[0] && isLuhnValidCardCandidate(match[0])) {
|
|
2455
|
+
return true;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
return false;
|
|
2459
|
+
}
|
|
2460
|
+
function replaceCreditCardCandidates(value, replacer) {
|
|
2461
|
+
return value.replace(/\b(?:\d[ -]*?){13,19}\b/g, (match, offset, fullText) => {
|
|
2462
|
+
if (shouldPreserveNumericCandidateInText(String(fullText), Number(offset), match)) {
|
|
2463
|
+
return match;
|
|
2464
|
+
}
|
|
2465
|
+
if (!isLuhnValidCardCandidate(match)) {
|
|
2466
|
+
return match;
|
|
2467
|
+
}
|
|
2468
|
+
return replacer(match);
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
function shouldPreserveNumericCandidateInText(value, start, match) {
|
|
2472
|
+
const token = expandTokenAround(value, start, start + match.length);
|
|
2473
|
+
if (!token) {
|
|
2474
|
+
return false;
|
|
2475
|
+
}
|
|
2476
|
+
if (/^\d{10,14}-[a-z0-9]+(?:-[a-z0-9]+)?$/i.test(token)) {
|
|
2477
|
+
return true;
|
|
2478
|
+
}
|
|
2479
|
+
if (/^(?:req|rid|trace|span|request|event)[-_]?\d{10,}/i.test(token)) {
|
|
2480
|
+
return true;
|
|
2481
|
+
}
|
|
2482
|
+
if (/^\d{10,14}$/.test(token) && !isLuhnValidCardCandidate(token)) {
|
|
2483
|
+
return true;
|
|
2484
|
+
}
|
|
2485
|
+
return shouldKeepOperationalStringExact(token);
|
|
2486
|
+
}
|
|
2487
|
+
function isLuhnValidCardCandidate(value) {
|
|
2488
|
+
const digits = value.replace(/\D/g, '');
|
|
2489
|
+
if (digits.length < 13 || digits.length > 19) {
|
|
2490
|
+
return false;
|
|
2491
|
+
}
|
|
2492
|
+
let sum = 0;
|
|
2493
|
+
let doubleDigit = false;
|
|
2494
|
+
for (let index = digits.length - 1; index >= 0; index -= 1) {
|
|
2495
|
+
let digit = Number(digits[index]);
|
|
2496
|
+
if (!Number.isInteger(digit)) {
|
|
2497
|
+
return false;
|
|
2498
|
+
}
|
|
2499
|
+
if (doubleDigit) {
|
|
2500
|
+
digit *= 2;
|
|
2501
|
+
if (digit > 9) {
|
|
2502
|
+
digit -= 9;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
sum += digit;
|
|
2506
|
+
doubleDigit = !doubleDigit;
|
|
2507
|
+
}
|
|
2508
|
+
return sum % 10 === 0;
|
|
2509
|
+
}
|
|
2510
|
+
function expandTokenAround(value, start, end) {
|
|
2511
|
+
let left = start;
|
|
2512
|
+
while (left > 0 && /[A-Za-z0-9_-]/.test(value[left - 1])) {
|
|
2513
|
+
left -= 1;
|
|
2514
|
+
}
|
|
2515
|
+
let right = end;
|
|
2516
|
+
while (right < value.length && /[A-Za-z0-9_-]/.test(value[right])) {
|
|
2517
|
+
right += 1;
|
|
2518
|
+
}
|
|
2519
|
+
return value.slice(left, right);
|
|
2520
|
+
}
|
|
2521
|
+
function mergeSpans(spans) {
|
|
2522
|
+
const sorted = spans.slice().sort((left, right) => left.start - right.start || left.end - right.end);
|
|
2523
|
+
const merged = [];
|
|
2524
|
+
for (const span of sorted) {
|
|
2525
|
+
const last = merged[merged.length - 1];
|
|
2526
|
+
if (!last || span.start >= last.end) {
|
|
2527
|
+
merged.push(span);
|
|
2528
|
+
continue;
|
|
2529
|
+
}
|
|
2530
|
+
if (span.end > last.end) {
|
|
2531
|
+
last.end = span.end;
|
|
2532
|
+
last.replacement = span.replacement;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
return merged;
|
|
2536
|
+
}
|
|
2537
|
+
function maskEmail(value) {
|
|
2538
|
+
return (0, privacy_redaction_1.redactEmailDeterministic)(value);
|
|
2539
|
+
}
|
|
2540
|
+
function maskCardLikeValue(value) {
|
|
2541
|
+
const digits = value.replace(/\D/g, '');
|
|
2542
|
+
if (!digits.length) {
|
|
2543
|
+
return '[masked]';
|
|
2544
|
+
}
|
|
2545
|
+
if (digits.length <= 4) {
|
|
2546
|
+
return `${digits.slice(0, 1)}...${digits.slice(-1)}`;
|
|
2547
|
+
}
|
|
2548
|
+
return `${digits.slice(0, 2)}...${digits.slice(-2)}`;
|
|
2549
|
+
}
|
|
2550
|
+
function stableToken(namespace, value, context) {
|
|
2551
|
+
const secret = context.appSecret || context.appId || 'repro';
|
|
2552
|
+
const material = `${secret}|${namespace}|${stableStringify(value)}`;
|
|
2553
|
+
return (0, crypto_1.createHash)('sha256').update(material).digest('hex').slice(0, 12);
|
|
2554
|
+
}
|
|
2555
|
+
function stableStringify(value) {
|
|
2556
|
+
if (value === undefined)
|
|
2557
|
+
return 'undefined';
|
|
2558
|
+
if (value === null)
|
|
2559
|
+
return 'null';
|
|
2560
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
2561
|
+
return String(value);
|
|
2562
|
+
}
|
|
2563
|
+
if (typeof value === 'bigint') {
|
|
2564
|
+
return `${value.toString()}n`;
|
|
2565
|
+
}
|
|
2566
|
+
if (Array.isArray(value)) {
|
|
2567
|
+
return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
|
2568
|
+
}
|
|
2569
|
+
if (value && typeof value === 'object') {
|
|
2570
|
+
return `{${Object.keys(value).sort().map((key) => `${key}:${stableStringify(value[key])}`).join(',')}}`;
|
|
2571
|
+
}
|
|
2572
|
+
return String(value);
|
|
2573
|
+
}
|
|
2574
|
+
function coercePrivacyObjectId(value) {
|
|
2575
|
+
if (value === null || value === undefined) {
|
|
2576
|
+
return null;
|
|
2577
|
+
}
|
|
2578
|
+
if (typeof value === 'string') {
|
|
2579
|
+
const direct = normalizePrivacyObjectIdString(value);
|
|
2580
|
+
return direct && /^[0-9a-f]{24}$/i.test(direct) ? direct.toLowerCase() : null;
|
|
2581
|
+
}
|
|
2582
|
+
if (typeof value !== 'object') {
|
|
2583
|
+
return null;
|
|
2584
|
+
}
|
|
2585
|
+
const bsonType = typeof value?._bsontype === 'string' ? String(value._bsontype).toLowerCase() : '';
|
|
2586
|
+
const ctorName = typeof value?.constructor?.name === 'string' ? String(value.constructor.name).toLowerCase() : '';
|
|
2587
|
+
const looksLikeObjectId = bsonType.includes('objectid') ||
|
|
2588
|
+
ctorName === 'objectid' ||
|
|
2589
|
+
typeof value.toHexString === 'function';
|
|
2590
|
+
const fromBufferShape = coercePrivacyObjectIdFromBufferShape(value, looksLikeObjectId);
|
|
2591
|
+
if (fromBufferShape) {
|
|
2592
|
+
return fromBufferShape;
|
|
2593
|
+
}
|
|
2594
|
+
if (typeof value.$oid === 'string') {
|
|
2595
|
+
const oid = normalizePrivacyObjectIdString(value.$oid);
|
|
2596
|
+
if (oid && /^[0-9a-f]{24}$/i.test(oid)) {
|
|
2597
|
+
return oid.toLowerCase();
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
try {
|
|
2601
|
+
if (typeof value.toHexString === 'function') {
|
|
2602
|
+
const hex = normalizePrivacyObjectIdString(value.toHexString());
|
|
2603
|
+
if (hex && /^[0-9a-f]{24}$/i.test(hex)) {
|
|
2604
|
+
return hex.toLowerCase();
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
catch { }
|
|
2609
|
+
try {
|
|
2610
|
+
if (typeof value.toString === 'function') {
|
|
2611
|
+
const text = normalizePrivacyObjectIdString(value.toString());
|
|
2612
|
+
if (text && /^[0-9a-f]{24}$/i.test(text)) {
|
|
2613
|
+
return text.toLowerCase();
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
catch { }
|
|
2618
|
+
return looksLikeObjectId ? '[mongo-id]' : null;
|
|
2619
|
+
}
|
|
2620
|
+
function normalizePrivacyObjectIdString(value) {
|
|
2621
|
+
if (value === null || value === undefined) {
|
|
2622
|
+
return null;
|
|
2623
|
+
}
|
|
2624
|
+
const text = String(value).trim();
|
|
2625
|
+
if (!text || text === '[object Object]') {
|
|
2626
|
+
return null;
|
|
2627
|
+
}
|
|
2628
|
+
return text;
|
|
2629
|
+
}
|
|
2630
|
+
function coercePrivacyObjectIdFromBufferShape(value, includeIdCandidate = false) {
|
|
2631
|
+
const decode = (source) => {
|
|
2632
|
+
if (!source) {
|
|
2633
|
+
return null;
|
|
2634
|
+
}
|
|
2635
|
+
if (typeof source === 'string') {
|
|
2636
|
+
const normalized = normalizePrivacyObjectIdString(source);
|
|
2637
|
+
return normalized && /^[0-9a-f]{24}$/i.test(normalized) ? normalized.toLowerCase() : null;
|
|
2638
|
+
}
|
|
2639
|
+
if ((typeof Buffer !== 'undefined' && Buffer.isBuffer(source)) ||
|
|
2640
|
+
source instanceof Uint8Array) {
|
|
2641
|
+
if (source.length !== 12) {
|
|
2642
|
+
return null;
|
|
2643
|
+
}
|
|
2644
|
+
try {
|
|
2645
|
+
return Buffer.from(source).toString('hex');
|
|
2646
|
+
}
|
|
2647
|
+
catch {
|
|
2648
|
+
return null;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
const bytes = Array.isArray(source)
|
|
2652
|
+
? source
|
|
2653
|
+
: typeof source === 'object' && source?.type === 'Buffer' && Array.isArray(source?.data)
|
|
2654
|
+
? source.data
|
|
2655
|
+
: typeof source === 'object'
|
|
2656
|
+
? Object.keys(source)
|
|
2657
|
+
.filter((key) => /^\d+$/.test(key))
|
|
2658
|
+
.sort((left, right) => Number(left) - Number(right))
|
|
2659
|
+
.map((key) => source[key])
|
|
2660
|
+
: null;
|
|
2661
|
+
if (!Array.isArray(bytes) || bytes.length !== 12) {
|
|
2662
|
+
return null;
|
|
2663
|
+
}
|
|
2664
|
+
const normalized = bytes.map((byte) => Number(byte));
|
|
2665
|
+
if (normalized.some((byte) => !Number.isInteger(byte) || byte < 0 || byte > 255)) {
|
|
2666
|
+
return null;
|
|
2667
|
+
}
|
|
2668
|
+
try {
|
|
2669
|
+
return Buffer.from(normalized).toString('hex');
|
|
2670
|
+
}
|
|
2671
|
+
catch {
|
|
2672
|
+
return null;
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
const candidates = includeIdCandidate
|
|
2676
|
+
? [value, value.buffer, value.id, value.$oid]
|
|
2677
|
+
: [value, value.buffer, value.$oid];
|
|
2678
|
+
for (const candidate of candidates) {
|
|
2679
|
+
const decoded = decode(candidate);
|
|
2680
|
+
if (decoded) {
|
|
2681
|
+
return decoded;
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
return null;
|
|
2685
|
+
}
|
|
2686
|
+
function looksLikeEmail(value) {
|
|
2687
|
+
return /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value.trim());
|
|
2688
|
+
}
|
|
2689
|
+
function looksLikeConnectionString(value) {
|
|
2690
|
+
return /^(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\/\/\S+$/i.test(value);
|
|
2691
|
+
}
|
|
2692
|
+
function containsJwtLikeText(value) {
|
|
2693
|
+
return /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/.test(value);
|
|
2694
|
+
}
|
|
2695
|
+
function replaceJwtLikeText(value) {
|
|
2696
|
+
return value.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, '[jwt]');
|
|
2697
|
+
}
|
|
2698
|
+
function containsBearerAuthText(value) {
|
|
2699
|
+
return /\bBearer\s+(?!\[(?:dropped|auth-token)\])[A-Za-z0-9._\-+/=]{8,}/i.test(value);
|
|
2700
|
+
}
|
|
2701
|
+
function replaceBearerAuthText(value) {
|
|
2702
|
+
return value.replace(/\bBearer\s+(?!\[(?:dropped|auth-token)\])[A-Za-z0-9._\-+/=]{8,}/gi, '[auth-token]');
|
|
2703
|
+
}
|
|
2704
|
+
function containsBasicAuthText(value) {
|
|
2705
|
+
return /\bBasic\s+(?!\[(?:dropped|basic-auth)\])[A-Za-z0-9+/=]{8,}/i.test(value);
|
|
2706
|
+
}
|
|
2707
|
+
function replaceBasicAuthText(value) {
|
|
2708
|
+
return value.replace(/\bBasic\s+(?!\[(?:dropped|basic-auth)\])[A-Za-z0-9+/=]{8,}/gi, '[basic-auth]');
|
|
2709
|
+
}
|
|
2710
|
+
function containsPrivateKeyBlock(value) {
|
|
2711
|
+
return /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/.test(value);
|
|
2712
|
+
}
|
|
2713
|
+
function replacePrivateKeyBlocks(value) {
|
|
2714
|
+
return value.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g, '[private-key]');
|
|
2715
|
+
}
|
|
2716
|
+
function normalizeTextPolicy(input) {
|
|
2717
|
+
if (!input || typeof input !== 'object') {
|
|
2718
|
+
return undefined;
|
|
2719
|
+
}
|
|
2720
|
+
const policy = input;
|
|
2721
|
+
if (policy.mode !== 'KNOWN_TEMPLATE' || !Array.isArray(policy.fragments) || !policy.fragments.length) {
|
|
2722
|
+
return undefined;
|
|
2723
|
+
}
|
|
2724
|
+
const fragments = policy.fragments
|
|
2725
|
+
.map((fragment) => normalizeTextFragment(fragment))
|
|
2726
|
+
.filter((fragment) => fragment !== null);
|
|
2727
|
+
if (!fragments.length) {
|
|
2728
|
+
return undefined;
|
|
2729
|
+
}
|
|
2730
|
+
return {
|
|
2731
|
+
mode: 'KNOWN_TEMPLATE',
|
|
2732
|
+
fragments,
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
function normalizeTextFragment(input) {
|
|
2736
|
+
if (!input || typeof input !== 'object') {
|
|
2737
|
+
return null;
|
|
2738
|
+
}
|
|
2739
|
+
const fragment = input;
|
|
2740
|
+
if (fragment.kind === 'literal') {
|
|
2741
|
+
return typeof fragment.text === 'string' && fragment.text.length
|
|
2742
|
+
? { kind: 'literal', text: fragment.text }
|
|
2743
|
+
: null;
|
|
2744
|
+
}
|
|
2745
|
+
if (fragment.kind === 'slot') {
|
|
2746
|
+
if (fragment.action !== 'KEEP_EXACT' &&
|
|
2747
|
+
fragment.action !== 'TOKENIZE' &&
|
|
2748
|
+
fragment.action !== 'PARTIAL_MASK' &&
|
|
2749
|
+
fragment.action !== 'DROP') {
|
|
2750
|
+
return null;
|
|
2751
|
+
}
|
|
2752
|
+
return {
|
|
2753
|
+
kind: 'slot',
|
|
2754
|
+
label: typeof fragment.label === 'string' && fragment.label.trim() ? fragment.label.trim() : undefined,
|
|
2755
|
+
action: fragment.action,
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
return null;
|
|
2759
|
+
}
|
|
2760
|
+
function normalizeRawTextMode(input) {
|
|
2761
|
+
return input === 'SAFE_PARTIAL_MASK' || input === 'REGEX_ASSISTED_EXACT'
|
|
2762
|
+
? input
|
|
2763
|
+
: undefined;
|
|
2764
|
+
}
|
|
2765
|
+
function normalizeRawTextHints(input) {
|
|
2766
|
+
if (!Array.isArray(input)) {
|
|
2767
|
+
return [];
|
|
2768
|
+
}
|
|
2769
|
+
const out = [];
|
|
2770
|
+
for (const entry of input) {
|
|
2771
|
+
const hint = normalizeRawTextHint(entry);
|
|
2772
|
+
if (hint) {
|
|
2773
|
+
out.push(hint);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
return out.slice(0, 50);
|
|
2777
|
+
}
|
|
2778
|
+
function normalizeRawTextHintNames(input) {
|
|
2779
|
+
if (!Array.isArray(input)) {
|
|
2780
|
+
return undefined;
|
|
2781
|
+
}
|
|
2782
|
+
const names = input
|
|
2783
|
+
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
2784
|
+
.filter(Boolean);
|
|
2785
|
+
return names.length ? Array.from(new Set(names)).slice(0, 50) : undefined;
|
|
2786
|
+
}
|
|
2787
|
+
function normalizeRawTextHint(input) {
|
|
2788
|
+
if (!input || typeof input !== 'object') {
|
|
2789
|
+
return null;
|
|
2790
|
+
}
|
|
2791
|
+
const record = input;
|
|
2792
|
+
if (record.scope !== 'RAW_TEXT_ONLY') {
|
|
2793
|
+
return null;
|
|
2794
|
+
}
|
|
2795
|
+
const action = record.action;
|
|
2796
|
+
if (action !== 'TOKENIZE' && action !== 'DROP') {
|
|
2797
|
+
return null;
|
|
2798
|
+
}
|
|
2799
|
+
const patternText = typeof record.regex === 'string'
|
|
2800
|
+
? record.regex
|
|
2801
|
+
: typeof record.pattern === 'string'
|
|
2802
|
+
? record.pattern
|
|
2803
|
+
: '';
|
|
2804
|
+
if (!isSafeRawTextRegexPattern(patternText)) {
|
|
2805
|
+
return null;
|
|
2806
|
+
}
|
|
2807
|
+
const pattern = safeRegExp(patternText);
|
|
2808
|
+
if (!pattern) {
|
|
2809
|
+
return null;
|
|
2810
|
+
}
|
|
2811
|
+
const name = typeof record.name === 'string' && record.name.trim()
|
|
2812
|
+
? record.name.trim()
|
|
2813
|
+
: 'rawTextHint';
|
|
2814
|
+
const tokenLabel = normalizeTokenLabel(typeof record.tokenLabel === 'string' && record.tokenLabel.trim()
|
|
2815
|
+
? record.tokenLabel
|
|
2816
|
+
: name);
|
|
2817
|
+
return {
|
|
2818
|
+
name,
|
|
2819
|
+
action,
|
|
2820
|
+
pattern,
|
|
2821
|
+
tokenLabel,
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
function isSafeRawTextRegexPattern(pattern) {
|
|
2825
|
+
const trimmed = pattern.trim();
|
|
2826
|
+
if (!trimmed || trimmed.length > 500) {
|
|
2827
|
+
return false;
|
|
2828
|
+
}
|
|
2829
|
+
if (trimmed === '.*' || trimmed === '.+' || trimmed === '[\\s\\S]*' || trimmed === '[\\s\\S]+') {
|
|
2830
|
+
return false;
|
|
2831
|
+
}
|
|
2832
|
+
if (/\(\?<?[=!]/.test(trimmed)) {
|
|
2833
|
+
return false;
|
|
2834
|
+
}
|
|
2835
|
+
if (/\.\s*[+*{]/.test(trimmed)) {
|
|
2836
|
+
return false;
|
|
2837
|
+
}
|
|
2838
|
+
if (/\[[^\]]+\]\s*[+*]/.test(trimmed)) {
|
|
2839
|
+
return false;
|
|
2840
|
+
}
|
|
2841
|
+
if (/\[[^\]]+\]\s*\{\d+,\}/.test(trimmed)) {
|
|
2842
|
+
return false;
|
|
2843
|
+
}
|
|
2844
|
+
if (/\([^)]*[+*][^)]*\)\s*[+*{]/.test(trimmed)) {
|
|
2845
|
+
return false;
|
|
2846
|
+
}
|
|
2847
|
+
return true;
|
|
2848
|
+
}
|
|
2849
|
+
function normalizeTokenLabel(value) {
|
|
2850
|
+
const normalized = value
|
|
2851
|
+
.trim()
|
|
2852
|
+
.replace(/[^A-Za-z0-9_]+/g, '_')
|
|
2853
|
+
.replace(/^_+|_+$/g, '');
|
|
2854
|
+
return normalized || 'rawTextHint';
|
|
2855
|
+
}
|
|
2856
|
+
function safeRegExp(pattern) {
|
|
2857
|
+
try {
|
|
2858
|
+
const inlineFlags = pattern.match(/^\(\?([imsu]+)\)/i);
|
|
2859
|
+
if (inlineFlags) {
|
|
2860
|
+
const normalizedFlags = Array.from(new Set(inlineFlags[1].toLowerCase().split(''))).join('');
|
|
2861
|
+
return new RegExp(pattern.slice(inlineFlags[0].length), normalizedFlags);
|
|
2862
|
+
}
|
|
2863
|
+
return new RegExp(pattern);
|
|
2864
|
+
}
|
|
2865
|
+
catch {
|
|
2866
|
+
return undefined;
|
|
2867
|
+
}
|
|
2868
|
+
}
|