@logtape/redaction 2.1.0-dev.600 → 2.1.0
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/dist/_virtual/rolldown_runtime.cjs +30 -0
- package/dist/field.cjs +326 -16
- package/dist/field.d.cts +102 -2
- package/dist/field.d.cts.map +1 -1
- package/dist/field.d.ts +102 -2
- package/dist/field.d.ts.map +1 -1
- package/dist/field.js +324 -16
- package/dist/field.js.map +1 -1
- package/dist/mod.cjs +2 -0
- package/dist/mod.d.cts +2 -2
- package/dist/mod.d.ts +2 -2
- package/dist/mod.js +2 -2
- package/package.json +2 -2
package/dist/field.d.ts
CHANGED
|
@@ -14,6 +14,21 @@ type FieldPattern = string | RegExp;
|
|
|
14
14
|
* @since 0.10.0
|
|
15
15
|
*/
|
|
16
16
|
type FieldPatterns = FieldPattern[];
|
|
17
|
+
/**
|
|
18
|
+
* The synchronous action to perform on a redacted field value.
|
|
19
|
+
* @since 0.10.0
|
|
20
|
+
*/
|
|
21
|
+
type FieldRedactionAction = "delete" | ((value: unknown) => unknown);
|
|
22
|
+
/**
|
|
23
|
+
* The asynchronous action to perform on a redacted field value.
|
|
24
|
+
* @since 2.1.0
|
|
25
|
+
*/
|
|
26
|
+
type AsyncFieldRedactionAction = (value: unknown) => PromiseLike<unknown>;
|
|
27
|
+
/**
|
|
28
|
+
* A pseudonymizer created by {@link createHmacPseudonymizer}.
|
|
29
|
+
* @since 2.1.0
|
|
30
|
+
*/
|
|
31
|
+
type HmacPseudonymizer = (value: unknown) => Promise<string>;
|
|
17
32
|
/**
|
|
18
33
|
* Default field patterns for redaction. These patterns will match
|
|
19
34
|
* common sensitive fields such as passwords, tokens, and personal
|
|
@@ -44,7 +59,50 @@ interface FieldRedactionOptions {
|
|
|
44
59
|
* properties.
|
|
45
60
|
* @default `"delete"`
|
|
46
61
|
*/
|
|
47
|
-
readonly action?:
|
|
62
|
+
readonly action?: FieldRedactionAction;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Options for asynchronously redacting fields in a {@link LogRecord}. Used by
|
|
66
|
+
* the {@link redactByFieldAsync} function.
|
|
67
|
+
* @since 2.1.0
|
|
68
|
+
*/
|
|
69
|
+
interface AsyncFieldRedactionOptions {
|
|
70
|
+
/**
|
|
71
|
+
* The field patterns to match against. This can be an array of
|
|
72
|
+
* strings or regular expressions. If a field matches any of the
|
|
73
|
+
* patterns, it will be redacted.
|
|
74
|
+
*/
|
|
75
|
+
readonly fieldPatterns: FieldPatterns;
|
|
76
|
+
/**
|
|
77
|
+
* The asynchronous action to perform on the matched fields.
|
|
78
|
+
*/
|
|
79
|
+
readonly action: AsyncFieldRedactionAction;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Options for the {@link createHmacPseudonymizer} function.
|
|
83
|
+
* @since 2.1.0
|
|
84
|
+
*/
|
|
85
|
+
interface HmacPseudonymizerOptions {
|
|
86
|
+
/**
|
|
87
|
+
* The secret key to use for HMAC. Strings are encoded as UTF-8.
|
|
88
|
+
*/
|
|
89
|
+
readonly key: string | Uint8Array | ArrayBuffer | CryptoKey;
|
|
90
|
+
/**
|
|
91
|
+
* The HMAC hash algorithm.
|
|
92
|
+
* @default `"SHA-256"`
|
|
93
|
+
*/
|
|
94
|
+
readonly hash?: "SHA-256" | "SHA-384" | "SHA-512";
|
|
95
|
+
/**
|
|
96
|
+
* The digest encoding.
|
|
97
|
+
* @default `"base64url"`
|
|
98
|
+
*/
|
|
99
|
+
readonly encoding?: "base64url" | "hex";
|
|
100
|
+
/**
|
|
101
|
+
* The string prefix to prepend to each pseudonym. If omitted, a prefix based
|
|
102
|
+
* on the hash algorithm is used, such as `"hmac-sha256:"`. Set this to an
|
|
103
|
+
* empty string to disable the prefix.
|
|
104
|
+
*/
|
|
105
|
+
readonly prefix?: string;
|
|
48
106
|
}
|
|
49
107
|
/**
|
|
50
108
|
* Redacts properties and message values in a {@link LogRecord} based on the
|
|
@@ -73,6 +131,48 @@ interface FieldRedactionOptions {
|
|
|
73
131
|
* @since 0.10.0
|
|
74
132
|
*/
|
|
75
133
|
declare function redactByField(sink: Sink | Sink & Disposable | Sink & AsyncDisposable, options?: FieldRedactionOptions | FieldPatterns): Sink | Sink & Disposable | Sink & AsyncDisposable;
|
|
134
|
+
/**
|
|
135
|
+
* Redacts properties and message values in a {@link LogRecord} based on the
|
|
136
|
+
* provided field patterns and asynchronous action.
|
|
137
|
+
*
|
|
138
|
+
* The returned sink preserves record ordering by processing redaction work in
|
|
139
|
+
* sequence and implements {@link AsyncDisposable} so callers can wait for all
|
|
140
|
+
* pending redaction work before shutdown.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* import { getConsoleSink } from "@logtape/logtape";
|
|
145
|
+
* import {
|
|
146
|
+
* createHmacPseudonymizer,
|
|
147
|
+
* redactByFieldAsync,
|
|
148
|
+
* } from "@logtape/redaction";
|
|
149
|
+
*
|
|
150
|
+
* const pseudonymize = await createHmacPseudonymizer({ key: "secret" });
|
|
151
|
+
* const sink = redactByFieldAsync(getConsoleSink(), {
|
|
152
|
+
* fieldPatterns: [/userId/i, /email/i],
|
|
153
|
+
* action: pseudonymize,
|
|
154
|
+
* });
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* @param sink The sink to wrap.
|
|
158
|
+
* @param options The async redaction options.
|
|
159
|
+
* @returns The wrapped sink.
|
|
160
|
+
* @since 2.1.0
|
|
161
|
+
*/
|
|
162
|
+
declare function redactByFieldAsync(sink: Sink | Sink & Disposable | Sink & AsyncDisposable, options: AsyncFieldRedactionOptions): Sink & AsyncDisposable;
|
|
163
|
+
/**
|
|
164
|
+
* Creates an asynchronous pseudonymizer based on HMAC using the Web Crypto API.
|
|
165
|
+
*
|
|
166
|
+
* The returned function converts each value with `String(value)`, encodes it
|
|
167
|
+
* as UTF-8, and returns a stable pseudonym. Because HMAC is keyed, this is
|
|
168
|
+
* safer than a plain salted hash for values with small input spaces, such as
|
|
169
|
+
* email addresses or numeric user IDs.
|
|
170
|
+
*
|
|
171
|
+
* @param options The HMAC pseudonymizer options.
|
|
172
|
+
* @returns An async redaction action.
|
|
173
|
+
* @since 2.1.0
|
|
174
|
+
*/
|
|
175
|
+
declare function createHmacPseudonymizer(options: HmacPseudonymizerOptions): Promise<HmacPseudonymizer>;
|
|
76
176
|
/**
|
|
77
177
|
* Redacts properties from an object based on specified field patterns.
|
|
78
178
|
*
|
|
@@ -90,5 +190,5 @@ declare function redactByField(sink: Sink | Sink & Disposable | Sink & AsyncDisp
|
|
|
90
190
|
* @since 0.10.0
|
|
91
191
|
*/
|
|
92
192
|
//#endregion
|
|
93
|
-
export { DEFAULT_REDACT_FIELDS, FieldPattern, FieldPatterns, FieldRedactionOptions, redactByField };
|
|
193
|
+
export { AsyncFieldRedactionAction, AsyncFieldRedactionOptions, DEFAULT_REDACT_FIELDS, FieldPattern, FieldPatterns, FieldRedactionAction, FieldRedactionOptions, HmacPseudonymizer, HmacPseudonymizerOptions, createHmacPseudonymizer, redactByField, redactByFieldAsync };
|
|
94
194
|
//# sourceMappingURL=field.d.ts.map
|
package/dist/field.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field.d.ts","names":[],"sources":["../src/field.ts"],"sourcesContent":[],"mappings":";;;;;;AAOA;AAOA;
|
|
1
|
+
{"version":3,"file":"field.d.ts","names":[],"sources":["../src/field.ts"],"sourcesContent":[],"mappings":";;;;;;AAOA;AAOA;AAMA;AAMY,KAnBA,YAAA,GAmBA,MAAyB,GAnBD,MAmBC;AAQrC;AAWA;AAqBA;;;AAmBoB,KAvER,aAAA,GAAgB,YAuER,EAAA;AAAoB;AAQxC;;;AAWmB,KApFP,oBAAA,GAoFO,QAAA,GAAA,CAAA,CAAA,KAAA,EAAA,OAAA,EAAA,GAAA,OAAA,CAAA;AAAyB;AAO5C;;;AAIsC,KAzF1B,yBAAA,GAyF0B,CAAA,KAAA,EAAA,OAAA,EAAA,GAvFjC,WAuFiC,CAAA,OAAA,CAAA;;AAAuB;AAgD7D;;AACQ,KAlII,iBAAA,GAkIJ,CAAA,KAAA,EAAA,OAAA,EAAA,GAlI4C,OAkI5C,CAAA,MAAA,CAAA;;;;;;;AAEL,cAzHU,qBAyHV,EAzHiC,aAyHjC;;;;;AAAiD;AA0EpC,UA9KC,qBAAA,CA8KiB;EAAA;;;;;;EACuB,SAC9C,aAAA,EAzKe,aAyKf;EAA0B;;AACZ;AAkIzB;;;;;AAEU;;oBAlSU;;;;;;;UAQH,0BAAA;;;;;;0BAMS;;;;mBAKP;;;;;;UAOF,wBAAA;;;;yBAIQ,aAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAgDpC,aAAA,OACR,OAAO,OAAO,aAAa,OAAO,2BAC/B,wBAAwB,gBAChC,OAAO,OAAO,aAAa,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0ErB,kBAAA,OACR,OAAO,OAAO,aAAa,OAAO,0BAC/B,6BACR,OAAO;;;;;;;;;;;;;iBAkIY,uBAAA,UACX,2BACR,QAAQ"}
|
package/dist/field.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { getLogger } from "@logtape/logtape";
|
|
2
|
+
|
|
1
3
|
//#region src/field.ts
|
|
4
|
+
const metaLogger = getLogger(["logtape", "meta"]);
|
|
5
|
+
let reportingRedactionFailure = false;
|
|
2
6
|
/**
|
|
3
7
|
* Default field patterns for redaction. These patterns will match
|
|
4
8
|
* common sensitive fields such as passwords, tokens, and personal
|
|
@@ -70,6 +74,133 @@ function redactByField(sink, options = DEFAULT_REDACT_FIELDS) {
|
|
|
70
74
|
return wrapped;
|
|
71
75
|
}
|
|
72
76
|
/**
|
|
77
|
+
* Redacts properties and message values in a {@link LogRecord} based on the
|
|
78
|
+
* provided field patterns and asynchronous action.
|
|
79
|
+
*
|
|
80
|
+
* The returned sink preserves record ordering by processing redaction work in
|
|
81
|
+
* sequence and implements {@link AsyncDisposable} so callers can wait for all
|
|
82
|
+
* pending redaction work before shutdown.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* import { getConsoleSink } from "@logtape/logtape";
|
|
87
|
+
* import {
|
|
88
|
+
* createHmacPseudonymizer,
|
|
89
|
+
* redactByFieldAsync,
|
|
90
|
+
* } from "@logtape/redaction";
|
|
91
|
+
*
|
|
92
|
+
* const pseudonymize = await createHmacPseudonymizer({ key: "secret" });
|
|
93
|
+
* const sink = redactByFieldAsync(getConsoleSink(), {
|
|
94
|
+
* fieldPatterns: [/userId/i, /email/i],
|
|
95
|
+
* action: pseudonymize,
|
|
96
|
+
* });
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @param sink The sink to wrap.
|
|
100
|
+
* @param options The async redaction options.
|
|
101
|
+
* @returns The wrapped sink.
|
|
102
|
+
* @since 2.1.0
|
|
103
|
+
*/
|
|
104
|
+
function redactByFieldAsync(sink, options) {
|
|
105
|
+
let lastPromise = Promise.resolve();
|
|
106
|
+
let closed = false;
|
|
107
|
+
const sinkErrors = [];
|
|
108
|
+
const wrapped = (record) => {
|
|
109
|
+
if (closed) return;
|
|
110
|
+
const work = redactLogRecordAsync(record, options).catch((error) => {
|
|
111
|
+
reportRedactionFailure(error);
|
|
112
|
+
return null;
|
|
113
|
+
});
|
|
114
|
+
lastPromise = lastPromise.then(async () => {
|
|
115
|
+
const result = await work;
|
|
116
|
+
if (result == null) return;
|
|
117
|
+
try {
|
|
118
|
+
await sink({
|
|
119
|
+
...record,
|
|
120
|
+
message: result.message,
|
|
121
|
+
properties: result.properties
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
sinkErrors.push(error);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
wrapped[Symbol.asyncDispose] = async () => {
|
|
129
|
+
closed = true;
|
|
130
|
+
await lastPromise;
|
|
131
|
+
let disposeError;
|
|
132
|
+
try {
|
|
133
|
+
if (Symbol.asyncDispose in sink) await sink[Symbol.asyncDispose]();
|
|
134
|
+
else if (Symbol.dispose in sink) sink[Symbol.dispose]();
|
|
135
|
+
} catch (error) {
|
|
136
|
+
disposeError = error;
|
|
137
|
+
}
|
|
138
|
+
if (sinkErrors.length > 0) {
|
|
139
|
+
const errors = disposeError == null ? sinkErrors : [...sinkErrors, disposeError];
|
|
140
|
+
if (errors.length === 1) throw errors[0];
|
|
141
|
+
throw new AggregateError(errors, "One or more errors occurred while emitting redacted log records.");
|
|
142
|
+
}
|
|
143
|
+
if (disposeError != null) throw disposeError;
|
|
144
|
+
};
|
|
145
|
+
return wrapped;
|
|
146
|
+
}
|
|
147
|
+
async function redactLogRecordAsync(record, options) {
|
|
148
|
+
const redactedProperties = await redactPropertiesAsync(record.properties, options);
|
|
149
|
+
if (typeof record.rawMessage === "string") {
|
|
150
|
+
const placeholders = extractPlaceholderNames(record.rawMessage);
|
|
151
|
+
const { redactedIndices, wildcardIndices } = getRedactedPlaceholderIndices(placeholders, options.fieldPatterns);
|
|
152
|
+
return {
|
|
153
|
+
message: await redactMessageArrayAsync(record.message, placeholders, redactedIndices, wildcardIndices, redactedProperties, options.action),
|
|
154
|
+
properties: redactedProperties
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
let redactedMessage = record.message;
|
|
158
|
+
const redactedValues = getRedactedValues(record.properties, redactedProperties);
|
|
159
|
+
if (redactedValues.size > 0) redactedMessage = redactMessageByValues(record.message, redactedValues);
|
|
160
|
+
return {
|
|
161
|
+
message: redactedMessage,
|
|
162
|
+
properties: redactedProperties
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function reportRedactionFailure(error) {
|
|
166
|
+
if (reportingRedactionFailure || typeof metaLogger.warn !== "function") return;
|
|
167
|
+
try {
|
|
168
|
+
reportingRedactionFailure = true;
|
|
169
|
+
metaLogger.warn("Failed to redact a log record; dropping the record to avoid leaking sensitive data: {error}", { error });
|
|
170
|
+
} catch {} finally {
|
|
171
|
+
reportingRedactionFailure = false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Creates an asynchronous pseudonymizer based on HMAC using the Web Crypto API.
|
|
176
|
+
*
|
|
177
|
+
* The returned function converts each value with `String(value)`, encodes it
|
|
178
|
+
* as UTF-8, and returns a stable pseudonym. Because HMAC is keyed, this is
|
|
179
|
+
* safer than a plain salted hash for values with small input spaces, such as
|
|
180
|
+
* email addresses or numeric user IDs.
|
|
181
|
+
*
|
|
182
|
+
* @param options The HMAC pseudonymizer options.
|
|
183
|
+
* @returns An async redaction action.
|
|
184
|
+
* @since 2.1.0
|
|
185
|
+
*/
|
|
186
|
+
async function createHmacPseudonymizer(options) {
|
|
187
|
+
const subtle = globalThis.crypto?.subtle;
|
|
188
|
+
if (subtle == null) throw new TypeError("The Web Crypto API is not available.");
|
|
189
|
+
const hash = getHmacHash(options.key, options.hash);
|
|
190
|
+
const encoding = options.encoding ?? "base64url";
|
|
191
|
+
const prefix = options.prefix ?? `hmac-${hash.toLowerCase().replaceAll("-", "")}:`;
|
|
192
|
+
const key = isCryptoKey(options.key) ? options.key : await subtle.importKey("raw", keyToBytes(options.key), {
|
|
193
|
+
name: "HMAC",
|
|
194
|
+
hash
|
|
195
|
+
}, false, ["sign"]);
|
|
196
|
+
const encoder = new TextEncoder();
|
|
197
|
+
return async (value) => {
|
|
198
|
+
const data = encoder.encode(String(value));
|
|
199
|
+
const signature = new Uint8Array(await subtle.sign("HMAC", key, data));
|
|
200
|
+
return prefix + encodeBytes(signature, encoding);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
73
204
|
* Redacts properties from an object based on specified field patterns.
|
|
74
205
|
*
|
|
75
206
|
* This function creates a shallow copy of the input object and applies
|
|
@@ -89,7 +220,7 @@ function redactProperties(properties, options, visited = /* @__PURE__ */ new Map
|
|
|
89
220
|
if (visited.has(properties)) return visited.get(properties);
|
|
90
221
|
const copy = {};
|
|
91
222
|
visited.set(properties, copy);
|
|
92
|
-
for (const field
|
|
223
|
+
for (const field of Object.keys(properties)) {
|
|
93
224
|
if (shouldFieldRedacted(field, options.fieldPatterns)) {
|
|
94
225
|
if (typeof options.action === "function") setProperty(copy, field, options.action(properties[field]));
|
|
95
226
|
continue;
|
|
@@ -102,6 +233,36 @@ function redactProperties(properties, options, visited = /* @__PURE__ */ new Map
|
|
|
102
233
|
}
|
|
103
234
|
return copy;
|
|
104
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Redacts properties from an object using an asynchronous action.
|
|
238
|
+
* @param properties The properties to redact.
|
|
239
|
+
* @param options The async redaction options.
|
|
240
|
+
* @param visited Map of visited objects to prevent circular reference issues.
|
|
241
|
+
* @returns The redacted properties.
|
|
242
|
+
* @since 2.1.0
|
|
243
|
+
*/
|
|
244
|
+
async function redactPropertiesAsync(properties, options, visited = /* @__PURE__ */ new Map()) {
|
|
245
|
+
if (visited.has(properties)) return visited.get(properties);
|
|
246
|
+
const copy = {};
|
|
247
|
+
visited.set(properties, copy);
|
|
248
|
+
const fields = [];
|
|
249
|
+
const values = [];
|
|
250
|
+
for (const field of Object.keys(properties)) {
|
|
251
|
+
fields.push(field);
|
|
252
|
+
if (shouldFieldRedacted(field, options.fieldPatterns)) {
|
|
253
|
+
values.push(Promise.resolve(options.action(properties[field])));
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const value = properties[field];
|
|
257
|
+
if (Array.isArray(value)) values.push(redactArrayAsync(value, options, visited));
|
|
258
|
+
else if (typeof value === "object" && value !== null) if (isBuiltInObject(value)) values.push(Promise.resolve(value));
|
|
259
|
+
else values.push(redactPropertiesAsync(value, options, visited));
|
|
260
|
+
else values.push(Promise.resolve(value));
|
|
261
|
+
}
|
|
262
|
+
const redactedValues = await Promise.all(values);
|
|
263
|
+
for (let i = 0; i < fields.length; i++) setProperty(copy, fields[i], redactedValues[i]);
|
|
264
|
+
return copy;
|
|
265
|
+
}
|
|
105
266
|
function setProperty(object, field, value) {
|
|
106
267
|
if (field === "__proto__") Object.defineProperty(object, field, {
|
|
107
268
|
value,
|
|
@@ -119,14 +280,40 @@ function setProperty(object, field, value) {
|
|
|
119
280
|
* @returns A new array with redacted values.
|
|
120
281
|
*/
|
|
121
282
|
function redactArray(array, options, visited) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
283
|
+
if (visited.has(array)) return visited.get(array);
|
|
284
|
+
const copy = [];
|
|
285
|
+
copy.length = array.length;
|
|
286
|
+
visited.set(array, copy);
|
|
287
|
+
for (let i = 0; i < array.length; i++) {
|
|
288
|
+
if (!(i in array)) continue;
|
|
289
|
+
const item = array[i];
|
|
290
|
+
if (Array.isArray(item)) copy[i] = redactArray(item, options, visited);
|
|
291
|
+
else if (typeof item === "object" && item !== null) if (isBuiltInObject(item)) copy[i] = item;
|
|
292
|
+
else copy[i] = redactProperties(item, options, visited);
|
|
293
|
+
else copy[i] = item;
|
|
294
|
+
}
|
|
295
|
+
return copy;
|
|
296
|
+
}
|
|
297
|
+
async function redactArrayAsync(array, options, visited) {
|
|
298
|
+
if (visited.has(array)) return visited.get(array);
|
|
299
|
+
const copy = [];
|
|
300
|
+
copy.length = array.length;
|
|
301
|
+
visited.set(array, copy);
|
|
302
|
+
const itemPromises = [];
|
|
303
|
+
for (let i = 0; i < array.length; i++) {
|
|
304
|
+
if (!(i in array)) {
|
|
305
|
+
itemPromises.push(Promise.resolve(void 0));
|
|
306
|
+
continue;
|
|
127
307
|
}
|
|
128
|
-
|
|
129
|
-
|
|
308
|
+
const item = array[i];
|
|
309
|
+
if (Array.isArray(item)) itemPromises.push(redactArrayAsync(item, options, visited));
|
|
310
|
+
else if (typeof item === "object" && item !== null) if (isBuiltInObject(item)) itemPromises.push(Promise.resolve(item));
|
|
311
|
+
else itemPromises.push(redactPropertiesAsync(item, options, visited));
|
|
312
|
+
else itemPromises.push(Promise.resolve(item));
|
|
313
|
+
}
|
|
314
|
+
const redactedItems = await Promise.all(itemPromises);
|
|
315
|
+
for (let i = 0; i < redactedItems.length; i++) if (i in array) copy[i] = redactedItems[i];
|
|
316
|
+
return copy;
|
|
130
317
|
}
|
|
131
318
|
/**
|
|
132
319
|
* Checks if a value is a built-in object that should not be recursively
|
|
@@ -187,11 +374,51 @@ function extractPlaceholderNames(template) {
|
|
|
187
374
|
function parsePathSegments(path) {
|
|
188
375
|
const segments = [];
|
|
189
376
|
let current = "";
|
|
190
|
-
|
|
191
|
-
|
|
377
|
+
let inBracket = false;
|
|
378
|
+
let quotedBracketSegment = false;
|
|
379
|
+
let quote;
|
|
380
|
+
let escaped = false;
|
|
381
|
+
const pushCurrent = (trim = false) => {
|
|
382
|
+
const segment = trim ? current.trimEnd() : current;
|
|
383
|
+
if (segment) segments.push(segment);
|
|
192
384
|
current = "";
|
|
193
|
-
}
|
|
194
|
-
|
|
385
|
+
};
|
|
386
|
+
for (const char of path) {
|
|
387
|
+
if (quote != null) {
|
|
388
|
+
if (escaped) {
|
|
389
|
+
current += char;
|
|
390
|
+
escaped = false;
|
|
391
|
+
} else if (char === "\\") escaped = true;
|
|
392
|
+
else if (char === quote) quote = void 0;
|
|
393
|
+
else current += char;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (inBracket && current === "" && /\s/.test(char)) continue;
|
|
397
|
+
if (inBracket && (char === "\"" || char === "'") && current === "") {
|
|
398
|
+
quote = char;
|
|
399
|
+
quotedBracketSegment = true;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (inBracket && quotedBracketSegment && /\s/.test(char)) continue;
|
|
403
|
+
if (char === "." && !inBracket) {
|
|
404
|
+
pushCurrent();
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (char === "[") {
|
|
408
|
+
pushCurrent();
|
|
409
|
+
inBracket = true;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (char === "]") {
|
|
413
|
+
pushCurrent(!quotedBracketSegment);
|
|
414
|
+
inBracket = false;
|
|
415
|
+
quotedBracketSegment = false;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (char === "?") continue;
|
|
419
|
+
current += char;
|
|
420
|
+
}
|
|
421
|
+
pushCurrent();
|
|
195
422
|
return segments;
|
|
196
423
|
}
|
|
197
424
|
/**
|
|
@@ -246,14 +473,51 @@ function redactMessageArray(message, placeholders, redactedIndices, wildcardIndi
|
|
|
246
473
|
else result.push(action(message[i]));
|
|
247
474
|
else {
|
|
248
475
|
const placeholderName = placeholders[placeholderIndex];
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
476
|
+
const redactedValue = getPathValue(redactedProperties, placeholderName);
|
|
477
|
+
if (redactedValue.found) result.push(redactedValue.value);
|
|
478
|
+
else result.push(message[i]);
|
|
479
|
+
}
|
|
480
|
+
placeholderIndex++;
|
|
481
|
+
}
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
async function redactMessageArrayAsync(message, placeholders, redactedIndices, wildcardIndices, redactedProperties, action) {
|
|
485
|
+
const result = [];
|
|
486
|
+
const tasks = [];
|
|
487
|
+
let placeholderIndex = 0;
|
|
488
|
+
for (let i = 0; i < message.length; i++) if (i % 2 === 0) result.push(message[i]);
|
|
489
|
+
else {
|
|
490
|
+
if (wildcardIndices.has(placeholderIndex)) result.push(redactedProperties);
|
|
491
|
+
else if (redactedIndices.has(placeholderIndex)) {
|
|
492
|
+
const index = result.length;
|
|
493
|
+
result.push(void 0);
|
|
494
|
+
tasks.push(Promise.resolve(action(message[i])).then((redacted) => {
|
|
495
|
+
result[index] = redacted;
|
|
496
|
+
}));
|
|
497
|
+
} else {
|
|
498
|
+
const placeholderName = placeholders[placeholderIndex];
|
|
499
|
+
const redactedValue = getPathValue(redactedProperties, placeholderName);
|
|
500
|
+
if (redactedValue.found) result.push(redactedValue.value);
|
|
251
501
|
else result.push(message[i]);
|
|
252
502
|
}
|
|
253
503
|
placeholderIndex++;
|
|
254
504
|
}
|
|
505
|
+
await Promise.all(tasks);
|
|
255
506
|
return result;
|
|
256
507
|
}
|
|
508
|
+
function getPathValue(properties, path) {
|
|
509
|
+
const segments = parsePathSegments(path);
|
|
510
|
+
if (segments.length < 1) return { found: false };
|
|
511
|
+
let value = properties;
|
|
512
|
+
for (const segment of segments) {
|
|
513
|
+
if (typeof value !== "object" && typeof value !== "function" || value == null || !Object.hasOwn(value, segment)) return { found: false };
|
|
514
|
+
value = value[segment];
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
found: true,
|
|
518
|
+
value
|
|
519
|
+
};
|
|
520
|
+
}
|
|
257
521
|
/**
|
|
258
522
|
* Collects redacted value mappings from original to redacted properties.
|
|
259
523
|
* @param original The original properties.
|
|
@@ -261,7 +525,7 @@ function redactMessageArray(message, placeholders, redactedIndices, wildcardIndi
|
|
|
261
525
|
* @param map The map to populate with original -> redacted value pairs.
|
|
262
526
|
*/
|
|
263
527
|
function collectRedactedValues(original, redacted, map) {
|
|
264
|
-
for (const key
|
|
528
|
+
for (const key of Object.keys(original)) {
|
|
265
529
|
const origVal = original[key];
|
|
266
530
|
const redVal = redacted[key];
|
|
267
531
|
if (origVal !== redVal) map.set(origVal, redVal);
|
|
@@ -297,7 +561,51 @@ function redactMessageByValues(message, redactedValues) {
|
|
|
297
561
|
}
|
|
298
562
|
return result;
|
|
299
563
|
}
|
|
564
|
+
function keyToBytes(key) {
|
|
565
|
+
if (typeof key === "string") return new TextEncoder().encode(key);
|
|
566
|
+
if (key instanceof ArrayBuffer) return key;
|
|
567
|
+
return key;
|
|
568
|
+
}
|
|
569
|
+
function isCryptoKey(key) {
|
|
570
|
+
return typeof key === "object" && key !== null && Object.prototype.toString.call(key) === "[object CryptoKey]";
|
|
571
|
+
}
|
|
572
|
+
function getHmacHash(key, hash) {
|
|
573
|
+
if (!isCryptoKey(key)) return hash ?? "SHA-256";
|
|
574
|
+
if (!key.usages.includes("sign")) throw new TypeError("The HMAC CryptoKey must include the \"sign\" usage.");
|
|
575
|
+
const keyHash = getCryptoKeyHmacHash(key);
|
|
576
|
+
if (hash != null && hash !== keyHash) throw new TypeError(`The HMAC CryptoKey uses ${keyHash}, but the "hash" option is ${hash}.`);
|
|
577
|
+
return keyHash;
|
|
578
|
+
}
|
|
579
|
+
function getCryptoKeyHmacHash(key) {
|
|
580
|
+
const algorithm = key.algorithm;
|
|
581
|
+
if (algorithm.name !== "HMAC" || !("hash" in algorithm)) throw new TypeError("The CryptoKey must be an HMAC key.");
|
|
582
|
+
const hashAlgorithm = algorithm.hash;
|
|
583
|
+
if (typeof hashAlgorithm !== "object" || hashAlgorithm == null || !("name" in hashAlgorithm) || typeof hashAlgorithm.name !== "string") throw new TypeError("The CryptoKey must specify an HMAC hash algorithm.");
|
|
584
|
+
const hash = hashAlgorithm.name;
|
|
585
|
+
switch (hash) {
|
|
586
|
+
case "SHA-256":
|
|
587
|
+
case "SHA-384":
|
|
588
|
+
case "SHA-512": return hash;
|
|
589
|
+
default: throw new TypeError(`Unsupported HMAC hash algorithm: ${hash}.`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function encodeBytes(bytes, encoding) {
|
|
593
|
+
switch (encoding) {
|
|
594
|
+
case "base64url": return encodeBase64Url(bytes);
|
|
595
|
+
case "hex": return encodeHex(bytes);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function encodeBase64Url(bytes) {
|
|
599
|
+
let binary = "";
|
|
600
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
601
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
602
|
+
}
|
|
603
|
+
function encodeHex(bytes) {
|
|
604
|
+
let result = "";
|
|
605
|
+
for (const byte of bytes) result += byte.toString(16).padStart(2, "0");
|
|
606
|
+
return result;
|
|
607
|
+
}
|
|
300
608
|
|
|
301
609
|
//#endregion
|
|
302
|
-
export { DEFAULT_REDACT_FIELDS, redactByField };
|
|
610
|
+
export { DEFAULT_REDACT_FIELDS, createHmacPseudonymizer, redactByField, redactByFieldAsync };
|
|
303
611
|
//# sourceMappingURL=field.js.map
|