1
import type { UnknownObject } from '@directus/types';
2
import { isObject } from '@directus/utils';
5
type Values = Record<string, any>;
6
type Replacement = (key?: string) => string;
9
* Redact values in an object.
11
* @param input Input object in which values should be redacted.
12
* @param redact The key paths at which and values itself which should be redacted.
13
* @param redact.keys Nested array of key paths at which values should be redacted. (Supports `*` for shallow matching, `**` for deep matching.)
14
* @param redact.values Value names and the corresponding values that should be redacted.
15
* @param replacement Replacement function with which the values are redacted.
16
* @returns Redacted object.
18
export function redactObject(
24
replacement: Replacement,
26
const wildcardChars = ['*', '**'];
28
const clone = JSON.parse(JSON.stringify(input, getReplacer(replacement, redact.values)));
31
traverse(clone, redact.keys);
36
function traverse(object: UnknownObject, checkKeyPaths: Keys): void {
37
if (checkKeyPaths.length === 0) {
41
const REDACTED_TEXT = replacement();
42
const globalCheckPaths = [];
44
for (const key of Object.keys(object)) {
45
const localCheckPaths = [];
47
for (const [index, path] of [...checkKeyPaths].entries()) {
48
const [current, ...remaining] = path;
50
const escapedKey = wildcardChars.includes(key) ? `\\${key}` : key;
54
if (remaining.length > 0) {
55
localCheckPaths.push(remaining);
57
object[key] = REDACTED_TEXT;
58
checkKeyPaths.splice(index, 1);
63
if (remaining.length > 0) {
64
globalCheckPaths.push(remaining);
65
checkKeyPaths.splice(index, 1);
67
object[key] = REDACTED_TEXT;
72
if (remaining.length > 0) {
73
const [next, ...nextRemaining] = remaining;
75
if (next === escapedKey) {
76
if (nextRemaining.length === 0) {
77
object[key] = REDACTED_TEXT;
79
localCheckPaths.push(nextRemaining);
81
} else if (next !== undefined && wildcardChars.includes(next)) {
82
localCheckPaths.push(remaining);
84
localCheckPaths.push(path);
87
object[key] = REDACTED_TEXT;
94
const value = object[key];
96
if (isObject(value)) {
97
traverse(value, [...globalCheckPaths, ...localCheckPaths]);
104
* Replace values and extract Error objects for use with JSON.stringify()
106
export function getReplacer(replacement: Replacement, values?: Values) {
107
const filteredValues = values
108
? Object.entries(values).filter(([_k, v]) => typeof v === 'string' && v.length > 0)
111
const replacer = (seen: WeakSet<object>) => {
112
return function (_key: string, value: unknown) {
113
if (value instanceof Error) {
116
message: value.message,
122
if (value !== null && typeof value === 'object') {
123
if (seen.has(value)) {
129
const newValue: any = Array.isArray(value) ? [] : {};
131
for (const [key2, value2] of Object.entries(value)) {
132
if (typeof value2 === 'string') {
133
newValue[key2] = value2;
135
newValue[key2] = replacer(seen)(key2, value2);
144
if (!values || filteredValues.length === 0 || typeof value !== 'string') return value;
146
let finalValue = value;
148
for (const [redactKey, valueToRedact] of filteredValues) {
149
if (finalValue.includes(valueToRedact)) {
150
finalValue = finalValue.replace(new RegExp(valueToRedact, 'g'), replacement(redactKey));
158
const seen = new WeakSet();
159
return replacer(seen);