drupal-mcp-connector 0.6.1

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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Shared helpers for the reporting tools — backend-neutral.
3
+ */
4
+
5
+ /** Canonical base property names (read directly off the entity, not from `fields`). */
6
+ const BASE_KEYS = new Set(["id", "title", "status", "langcode", "created", "changed", "url"]);
7
+
8
+ /** Whole-day length in milliseconds, used by daysSince(). */
9
+ const MS_PER_DAY = 86400000;
10
+
11
+ /**
12
+ * Collect up to maxItems canonical entities by paging listEntities via offset.
13
+ * Stops early once a short page or a falsy hasNext signals the end of results.
14
+ * @param {{listEntities: Function}} backend Resolved backend with listEntities().
15
+ * @param {object} descriptor Query descriptor passed through to listEntities.
16
+ * @param {number} [maxItems] Hard cap on total entities returned.
17
+ * @param {number} [chunk] Per-request page size (default 50, matching JSON:API's cap).
18
+ * @returns {Promise<object[]>} Up to maxItems canonical entities.
19
+ */
20
+ export async function collectEntities(backend, descriptor, maxItems = 100, chunk = 50) {
21
+ const out = [];
22
+ let offset = 0;
23
+ for (;;) {
24
+ const limit = Math.min(chunk, maxItems - out.length);
25
+ if (limit <= 0) break;
26
+ const res = await backend.listEntities({ ...descriptor, page: { limit, offset } });
27
+ const batch = res.entities ?? [];
28
+ out.push(...batch);
29
+ if (batch.length < limit || !res.page?.hasNext) break;
30
+ offset += batch.length;
31
+ }
32
+ return out.slice(0, maxItems);
33
+ }
34
+
35
+ /**
36
+ * Build a structured "report unavailable" result (returned, not thrown, so a
37
+ * caller can report a gated feature without aborting a batch of reports).
38
+ * @param {string} report Report identifier.
39
+ * @param {string} backend Backend the report was attempted against.
40
+ * @param {string} reason Why the report cannot run here.
41
+ * @returns {{unavailable: true, report: string, backend: string, reason: string}}
42
+ */
43
+ export function gatedReport(report, backend, reason) {
44
+ return { unavailable: true, report, backend, reason };
45
+ }
46
+
47
+ /**
48
+ * Read a value from a canonical entity by trying base props then `fields`,
49
+ * returning the first candidate name that resolves to a defined value.
50
+ * @param {object} entity Canonical entity.
51
+ * @param {string[]} candidates Field/prop names to try, in priority order.
52
+ * @returns {*} The first matching value, or undefined if none match.
53
+ */
54
+ export function fieldValue(entity, candidates) {
55
+ const base = new Map(Object.entries(entity));
56
+ const fields = entity.fields ? new Map(Object.entries(entity.fields)) : new Map();
57
+ for (const name of candidates) {
58
+ if (BASE_KEYS.has(name)) {
59
+ if (base.has(name) && base.get(name) !== undefined) return base.get(name);
60
+ continue;
61
+ }
62
+ if (fields.has(name)) return fields.get(name);
63
+ }
64
+ return undefined;
65
+ }
66
+
67
+ /**
68
+ * Whole days elapsed between a date and now.
69
+ * @param {?(string|number|Date)} dateValue A date parseable by `new Date()`.
70
+ * @returns {?number} Whole days since the date, or null when no date is given.
71
+ */
72
+ export function daysSince(dateValue) {
73
+ if (!dateValue) return null;
74
+ return Math.floor((Date.now() - new Date(dateValue).getTime()) / MS_PER_DAY);
75
+ }
@@ -0,0 +1,475 @@
1
+ import { parse } from "graphql";
2
+
3
+ /**
4
+ * Security layer — connector-level access control.
5
+ *
6
+ * This is a SECOND layer of defense on top of Drupal's own permission system.
7
+ * It lets you restrict what the MCP server will even attempt, regardless of
8
+ * what the API credential is capable of doing.
9
+ *
10
+ * Configuration lives in config.json under each site's "security" key.
11
+ * See config/config.example.json for annotated examples.
12
+ *
13
+ * ─── Quick presets ────────────────────────────────────────────────────────
14
+ *
15
+ * "preset": "development" Everything allowed. Default if no security key.
16
+ * "preset": "content-editor" Create/edit nodes+media. No user mgmt, no deletes.
17
+ * "preset": "auditor" Read-only. All entity types. User fields redacted.
18
+ * "preset": "production-strict" Read-only. Explicit allowlist required. Redacts PII.
19
+ * "preset": "write-plane" Governed writes (no delete/mutations) on node, term, media.
20
+ *
21
+ * Presets can be overridden by adding explicit keys alongside them.
22
+ *
23
+ * ─── Explicit config keys ─────────────────────────────────────────────────
24
+ *
25
+ * readOnly true → reject all create/update/delete/graphql-mutation calls
26
+ * allowDestructive false → reject all delete operations
27
+ * allowGraphqlMutations false → reject drupal_graphql when mutation is detected
28
+ *
29
+ * allowedEntityTypes string[] | null null = allow all; array = allowlist
30
+ * deniedEntityTypes string[] always-blocked entity types
31
+ *
32
+ * entityRules object per-entity-type overrides:
33
+ * [entityType]:
34
+ * allowedOperations ["read","create","update","delete"] (or subset)
35
+ * allowedBundles string[] | null
36
+ * deniedBundles string[]
37
+ * redactedFields string[] stripped from ALL responses for this type
38
+ *
39
+ * globalRedactedFields string[] stripped from every response, every type
40
+ *
41
+ * ─── Field redaction ──────────────────────────────────────────────────────
42
+ *
43
+ * Redacted fields are replaced with "[REDACTED]" in response attributes.
44
+ * Recommended to include for user entities: "pass", "mail" (if PII concern).
45
+ * The connector never writes these fields either when redaction is active.
46
+ */
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Preset definitions
50
+ // ---------------------------------------------------------------------------
51
+
52
+ const PRESETS = {
53
+ development: {
54
+ readOnly: false,
55
+ allowDestructive: true,
56
+ allowGraphqlMutations: true,
57
+ allowedEntityTypes: null,
58
+ deniedEntityTypes: [],
59
+ entityRules: {},
60
+ globalRedactedFields: [],
61
+ },
62
+
63
+ "content-editor": {
64
+ readOnly: false,
65
+ allowDestructive: false, // no deletes
66
+ allowGraphqlMutations: false,
67
+ allowedEntityTypes: ["node", "media", "file", "taxonomy_term", "menu_link_content"],
68
+ deniedEntityTypes: ["user"],
69
+ entityRules: {
70
+ node: { allowedOperations: ["read", "create", "update"] },
71
+ media: { allowedOperations: ["read", "create", "update"] },
72
+ file: { allowedOperations: ["read", "create"] },
73
+ taxonomy_term: { allowedOperations: ["read", "create", "update"] },
74
+ },
75
+ globalRedactedFields: [],
76
+ },
77
+
78
+ auditor: {
79
+ readOnly: true,
80
+ allowDestructive: false,
81
+ allowGraphqlMutations: false,
82
+ allowedEntityTypes: null, // read any entity type
83
+ deniedEntityTypes: [],
84
+ entityRules: {
85
+ user: {
86
+ allowedOperations: ["read"],
87
+ redactedFields: ["pass", "mail"],
88
+ },
89
+ },
90
+ globalRedactedFields: [],
91
+ },
92
+
93
+ "production-strict": {
94
+ readOnly: true,
95
+ allowDestructive: false,
96
+ allowGraphqlMutations: false,
97
+ allowedEntityTypes: null, // set an explicit allowlist in your config
98
+ deniedEntityTypes: ["user"], // no user data at all
99
+ entityRules: {},
100
+ globalRedactedFields: ["pass", "mail", "field_private", "field_api_key", "field_token"],
101
+ },
102
+
103
+ "write-plane": {
104
+ // Mirrors the server-side governance profile for agent writes. The
105
+ // Drupal-side governance layer remains authoritative; this is defence in depth.
106
+ readOnly: false,
107
+ allowDestructive: false, // no deletes
108
+ allowGraphqlMutations: false, // writes go through the JSON:API plane
109
+ allowedEntityTypes: ["node", "taxonomy_term", "media"],
110
+ deniedEntityTypes: ["user"],
111
+ entityRules: {},
112
+ globalRedactedFields: ["pass", "mail"],
113
+ },
114
+ };
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Resolve effective security config for a site
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /**
121
+ * Resolve a site's effective security config by layering explicit `security`
122
+ * keys over the selected preset (explicit keys win; redacted-field lists merge).
123
+ * @param {object} site Site config (reads site.security).
124
+ * @returns {object} Effective security config used by the assert/redact helpers.
125
+ */
126
+ export function resolveSecurityConfig(site) {
127
+ const raw = site.security ?? {};
128
+ const preset = PRESETS[raw.preset ?? "development"] ?? PRESETS.development;
129
+
130
+ // Merge: explicit keys in site.security override the preset
131
+ return {
132
+ readOnly: raw.readOnly ?? preset.readOnly,
133
+ allowDestructive: raw.allowDestructive ?? preset.allowDestructive,
134
+ allowGraphqlMutations: raw.allowGraphqlMutations ?? preset.allowGraphqlMutations,
135
+ allowedEntityTypes: raw.allowedEntityTypes ?? preset.allowedEntityTypes,
136
+ deniedEntityTypes: raw.deniedEntityTypes ?? preset.deniedEntityTypes,
137
+ entityRules: mergeEntityRules(preset.entityRules, raw.entityRules ?? {}),
138
+ globalRedactedFields: [
139
+ ...(preset.globalRedactedFields ?? []),
140
+ ...(raw.globalRedactedFields ?? []),
141
+ ],
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Shallow-merge per-entity-type rule objects, with override rules taking
147
+ * priority over preset rules for each entity type.
148
+ * @param {object} base Preset entityRules.
149
+ * @param {object} override Site-supplied entityRules.
150
+ * @returns {object} Merged entityRules.
151
+ */
152
+ function mergeEntityRules(base, override) {
153
+ const entries = Object.entries(override).map(([entityType, rules]) => {
154
+ const baseRules = new Map(Object.entries(base)).get(entityType) ?? {};
155
+ return [entityType, { ...baseRules, ...rules }];
156
+ });
157
+ return { ...base, ...Object.fromEntries(entries) };
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // SecurityError
162
+ // ---------------------------------------------------------------------------
163
+
164
+ /** Error thrown when a connector-level security policy blocks an operation. */
165
+ export class SecurityError extends Error {
166
+ /** @param {string} message Human-readable reason the operation was blocked. */
167
+ constructor(message) {
168
+ super(message);
169
+ this.name = "SecurityError";
170
+ }
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Assertion helpers — throw SecurityError if a check fails
175
+ // ---------------------------------------------------------------------------
176
+
177
+ /**
178
+ * @param {object} secConfig Resolved security config.
179
+ * @param {string} operationLabel Label used in the error message.
180
+ * @returns {void}
181
+ * @throws {SecurityError} if the site is configured read-only.
182
+ */
183
+ export function assertNotReadOnly(secConfig, operationLabel) {
184
+ if (secConfig.readOnly) {
185
+ throw new SecurityError(
186
+ `This site is configured as read-only. Operation blocked: ${operationLabel}. ` +
187
+ "To enable writes, set security.readOnly = false in your config."
188
+ );
189
+ }
190
+ }
191
+
192
+ /**
193
+ * @param {object} secConfig Resolved security config.
194
+ * @param {string} entityType Entity type targeted by the delete.
195
+ * @param {string} id Entity id targeted by the delete.
196
+ * @returns {void}
197
+ * @throws {SecurityError} if destructive (delete) operations are disabled.
198
+ */
199
+ export function assertDestructiveAllowed(secConfig, entityType, id) {
200
+ if (!secConfig.allowDestructive) {
201
+ throw new SecurityError(
202
+ "Destructive operations (delete) are disabled for this site. " +
203
+ `Blocked: delete ${entityType} ${id}. ` +
204
+ "To enable, set security.allowDestructive = true in your config."
205
+ );
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Detect whether a GraphQL document contains a mutation operation.
211
+ * Uses a real parser (robust against multi-operation docs, comments, and
212
+ * leading whitespace); falls back to a token-aware regex if the document
213
+ * does not parse.
214
+ * @param {string} query GraphQL document text.
215
+ * @returns {boolean} True if any operation is a mutation.
216
+ */
217
+ function graphqlHasMutation(query) {
218
+ try {
219
+ const doc = parse(query);
220
+ return doc.definitions.some(
221
+ (d) => d.kind === "OperationDefinition" && d.operation === "mutation"
222
+ );
223
+ } catch {
224
+ // Unparseable — be conservative. Whole-string, token-aware (no ^/m anchors).
225
+ return /(^|[^A-Za-z0-9_])mutation\s+[A-Za-z_{]/.test(query);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * @param {object} secConfig Resolved security config.
231
+ * @param {string} query GraphQL document text.
232
+ * @returns {void} No-op for read-only (query) documents.
233
+ * @throws {SecurityError} if the document is a mutation and writes/mutations are disabled.
234
+ */
235
+ export function assertGraphqlMutationAllowed(secConfig, query) {
236
+ const isMutation = graphqlHasMutation(query);
237
+ if (!isMutation) return;
238
+
239
+ // A read-only site blocks GraphQL mutations with the same switch as JSON:API writes.
240
+ if (secConfig.readOnly) {
241
+ assertNotReadOnly(secConfig, "graphql mutation");
242
+ }
243
+ if (!secConfig.allowGraphqlMutations) {
244
+ throw new SecurityError(
245
+ "GraphQL mutations are disabled for this site. " +
246
+ "Set security.allowGraphqlMutations = true to enable."
247
+ );
248
+ }
249
+ }
250
+
251
+ /**
252
+ * @param {object} secConfig Resolved security config.
253
+ * @param {string} entityType Entity type to check.
254
+ * @returns {void}
255
+ * @throws {SecurityError} if the type is denied, or not in a configured allowlist.
256
+ */
257
+ export function assertEntityTypeAllowed(secConfig, entityType) {
258
+ // Check denylist first (takes priority over allowlist)
259
+ if (secConfig.deniedEntityTypes.includes(entityType)) {
260
+ throw new SecurityError(
261
+ `Access to entity type "${entityType}" is denied by security config (deniedEntityTypes).`
262
+ );
263
+ }
264
+
265
+ // Check allowlist
266
+ if (secConfig.allowedEntityTypes !== null) {
267
+ if (!secConfig.allowedEntityTypes.includes(entityType)) {
268
+ const allowed = secConfig.allowedEntityTypes.join(", ");
269
+ throw new SecurityError(
270
+ `Entity type "${entityType}" is not in the allowedEntityTypes list. ` +
271
+ `Allowed: ${allowed}`
272
+ );
273
+ }
274
+ }
275
+ }
276
+
277
+ /**
278
+ * @param {object} secConfig Resolved security config.
279
+ * @param {string} entityType Entity type owning the bundle.
280
+ * @param {string} bundle Bundle to check.
281
+ * @returns {void} No-op when the entity type has no bundle rules.
282
+ * @throws {SecurityError} if the bundle is denied, or not in a configured allowlist.
283
+ */
284
+ export function assertBundleAllowed(secConfig, entityType, bundle) {
285
+ const rules = new Map(Object.entries(secConfig.entityRules)).get(entityType);
286
+ if (!rules) return; // no rules = allowed
287
+
288
+ if (rules.deniedBundles?.includes(bundle)) {
289
+ throw new SecurityError(
290
+ `Bundle "${bundle}" of entity type "${entityType}" is in the deniedBundles list.`
291
+ );
292
+ }
293
+ if (rules.allowedBundles !== null && rules.allowedBundles !== undefined) {
294
+ if (!rules.allowedBundles.includes(bundle)) {
295
+ throw new SecurityError(
296
+ `Bundle "${bundle}" is not in allowedBundles for "${entityType}". ` +
297
+ `Allowed: ${rules.allowedBundles.join(", ")}`
298
+ );
299
+ }
300
+ }
301
+ }
302
+
303
+ /**
304
+ * @param {object} secConfig Resolved security config.
305
+ * @param {"read"|"create"|"update"|"delete"} operation Operation to check.
306
+ * @param {string} entityType Entity type the operation targets.
307
+ * @returns {void} No-op when the type has no allowedOperations restriction.
308
+ * @throws {SecurityError} if the operation is not in the type's allowedOperations.
309
+ */
310
+ export function assertOperationAllowed(secConfig, operation, entityType) {
311
+ const rules = new Map(Object.entries(secConfig.entityRules)).get(entityType);
312
+ if (!rules?.allowedOperations) return; // no restriction
313
+
314
+ if (!rules.allowedOperations.includes(operation)) {
315
+ throw new SecurityError(
316
+ `Operation "${operation}" is not allowed on entity type "${entityType}". ` +
317
+ `Allowed operations: ${rules.allowedOperations.join(", ")}`
318
+ );
319
+ }
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Composite assertion for read operations (most common)
324
+ // ---------------------------------------------------------------------------
325
+
326
+ /**
327
+ * Composite read gate: entity type, bundle (if given), and the "read" operation.
328
+ * @param {object} secConfig Resolved security config.
329
+ * @param {string} entityType Entity type to read.
330
+ * @param {string} [bundle] Bundle to read.
331
+ * @returns {void}
332
+ * @throws {SecurityError} if any underlying check fails.
333
+ */
334
+ export function assertReadAllowed(secConfig, entityType, bundle) {
335
+ assertEntityTypeAllowed(secConfig, entityType);
336
+ if (bundle) assertBundleAllowed(secConfig, entityType, bundle);
337
+ assertOperationAllowed(secConfig, "read", entityType);
338
+ }
339
+
340
+ /**
341
+ * Composite write gate: read-only switch, entity type, bundle (if given), and op.
342
+ * @param {object} secConfig Resolved security config.
343
+ * @param {"create"|"update"|"delete"} operation Write operation.
344
+ * @param {string} entityType Entity type to write.
345
+ * @param {string} [bundle] Bundle to write.
346
+ * @returns {void}
347
+ * @throws {SecurityError} if any underlying check fails.
348
+ */
349
+ export function assertWriteAllowed(secConfig, operation, entityType, bundle) {
350
+ assertNotReadOnly(secConfig, `${operation} ${entityType}`);
351
+ assertEntityTypeAllowed(secConfig, entityType);
352
+ if (bundle) assertBundleAllowed(secConfig, entityType, bundle);
353
+ assertOperationAllowed(secConfig, operation, entityType);
354
+ }
355
+
356
+ /**
357
+ * Composite delete gate: destructive switch plus the full write gate.
358
+ * @param {object} secConfig Resolved security config.
359
+ * @param {string} entityType Entity type to delete.
360
+ * @param {string} [bundle] Bundle to delete.
361
+ * @param {string} id Entity id to delete (used in the error message).
362
+ * @returns {void}
363
+ * @throws {SecurityError} if deletes are disabled or any write check fails.
364
+ */
365
+ export function assertDeleteAllowed(secConfig, entityType, bundle, id) {
366
+ assertDestructiveAllowed(secConfig, entityType, id);
367
+ assertWriteAllowed(secConfig, "delete", entityType, bundle);
368
+ }
369
+
370
+ // ---------------------------------------------------------------------------
371
+ // Field redaction
372
+ // ---------------------------------------------------------------------------
373
+
374
+ /**
375
+ * Redact sensitive fields from a JSON:API resource object (or array of them),
376
+ * replacing their `attributes` values with "[REDACTED]".
377
+ * @param {?(object|object[])} resource JSON:API resource(s) to redact.
378
+ * @param {object} secConfig Resolved security config (supplies the field lists).
379
+ * @param {string} entityType Entity type, used to pick per-type redacted fields.
380
+ * @returns {?(object|object[])} New resource object(s); originals are not mutated.
381
+ */
382
+ export function redactResource(resource, secConfig, entityType) {
383
+ if (!resource) return resource;
384
+
385
+ // Collect fields to redact for this entity type
386
+ const entityRules = new Map(Object.entries(secConfig.entityRules)).get(entityType) ?? {};
387
+ const fieldsToRedact = new Set([
388
+ ...(secConfig.globalRedactedFields ?? []),
389
+ ...(entityRules.redactedFields ?? []),
390
+ ]);
391
+
392
+ if (fieldsToRedact.size === 0) return resource;
393
+
394
+ function redactAttrs(obj) {
395
+ if (!obj?.attributes) return obj;
396
+ const attrs = Object.fromEntries(
397
+ Object.entries(obj.attributes).map(([k, v]) => [k, fieldsToRedact.has(k) ? "[REDACTED]" : v])
398
+ );
399
+ return { ...obj, attributes: attrs };
400
+ }
401
+
402
+ if (Array.isArray(resource)) return resource.map(redactAttrs);
403
+ return redactAttrs(resource);
404
+ }
405
+
406
+ /**
407
+ * Redact sensitive fields from a CANONICAL entity (base props + `fields`).
408
+ * Mirrors redactResource but for the API-neutral canonical shape.
409
+ * @param {?object} entity Canonical entity to redact.
410
+ * @param {object} secConfig Resolved security config (supplies the field lists).
411
+ * @param {string} entityType Entity type, used to pick per-type redacted fields.
412
+ * @returns {?object} New entity; original is not mutated.
413
+ */
414
+ export function redactCanonicalEntity(entity, secConfig, entityType) {
415
+ if (!entity) return entity;
416
+ const entityRulesMap = secConfig.entityRules ? new Map(Object.entries(secConfig.entityRules)) : new Map();
417
+ const entityRules = entityRulesMap.get(entityType) ?? {};
418
+ const fieldsToRedact = new Set([
419
+ ...(secConfig.globalRedactedFields ?? []),
420
+ ...(entityRules.redactedFields ?? []),
421
+ ]);
422
+ if (fieldsToRedact.size === 0) return entity;
423
+
424
+ const redactedFields = Object.fromEntries(
425
+ Object.entries(entity.fields ?? {}).map(([k, v]) => [k, fieldsToRedact.has(k) ? "[REDACTED]" : v])
426
+ );
427
+ const BASE_PROPS = ["title", "status", "langcode", "created", "changed", "url"];
428
+ const baseOverrides = Object.fromEntries(
429
+ BASE_PROPS.filter((p) => fieldsToRedact.has(p)).map((p) => [p, "[REDACTED]"])
430
+ );
431
+ return { ...entity, fields: redactedFields, ...baseOverrides };
432
+ }
433
+
434
+ /**
435
+ * Redact a full JSON:API response by redacting each item under `.data`.
436
+ * @param {?object} response JSON:API response with a `data` object or array.
437
+ * @param {object} secConfig Resolved security config.
438
+ * @param {string} entityType Entity type for the response data.
439
+ * @returns {?object} New response; original is not mutated.
440
+ */
441
+ export function redactResponse(response, secConfig, entityType) {
442
+ if (!response?.data) return response;
443
+ return {
444
+ ...response,
445
+ data: Array.isArray(response.data)
446
+ ? response.data.map((r) => redactResource(r, secConfig, entityType))
447
+ : redactResource(response.data, secConfig, entityType),
448
+ };
449
+ }
450
+
451
+ // ---------------------------------------------------------------------------
452
+ // Security summary tool (exposed as drupal_security_info)
453
+ // ---------------------------------------------------------------------------
454
+
455
+ /**
456
+ * Build a human-readable summary of a site's effective security policy
457
+ * (exposed via the drupal_security_info tool). Lists only policy settings —
458
+ * no credentials.
459
+ * @param {object} site Site config.
460
+ * @returns {object} Flat summary of the resolved security settings.
461
+ */
462
+ export function getSecuritySummary(site) {
463
+ const cfg = resolveSecurityConfig(site);
464
+ return {
465
+ site: site._name,
466
+ preset: site.security?.preset ?? "development (default)",
467
+ readOnly: cfg.readOnly,
468
+ allowDestructive: cfg.allowDestructive,
469
+ allowGraphqlMutations: cfg.allowGraphqlMutations,
470
+ allowedEntityTypes: cfg.allowedEntityTypes ?? "all",
471
+ deniedEntityTypes: cfg.deniedEntityTypes,
472
+ entityRules: cfg.entityRules,
473
+ globalRedactedFields: cfg.globalRedactedFields,
474
+ };
475
+ }