@logtape/redaction 2.1.0-dev.576 → 2.1.0-dev.613
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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
|
|
25
|
+
Object.defineProperty(exports, '__toESM', {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
get: function () {
|
|
28
|
+
return __toESM;
|
|
29
|
+
}
|
|
30
|
+
});
|
package/dist/field.cjs
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
2
|
+
const __logtape_logtape = require_rolldown_runtime.__toESM(require("@logtape/logtape"));
|
|
1
3
|
|
|
2
4
|
//#region src/field.ts
|
|
5
|
+
const metaLogger = (0, __logtape_logtape.getLogger)(["logtape", "meta"]);
|
|
6
|
+
let reportingRedactionFailure = false;
|
|
3
7
|
/**
|
|
4
8
|
* Default field patterns for redaction. These patterns will match
|
|
5
9
|
* common sensitive fields such as passwords, tokens, and personal
|
|
@@ -71,6 +75,133 @@ function redactByField(sink, options = DEFAULT_REDACT_FIELDS) {
|
|
|
71
75
|
return wrapped;
|
|
72
76
|
}
|
|
73
77
|
/**
|
|
78
|
+
* Redacts properties and message values in a {@link LogRecord} based on the
|
|
79
|
+
* provided field patterns and asynchronous action.
|
|
80
|
+
*
|
|
81
|
+
* The returned sink preserves record ordering by processing redaction work in
|
|
82
|
+
* sequence and implements {@link AsyncDisposable} so callers can wait for all
|
|
83
|
+
* pending redaction work before shutdown.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { getConsoleSink } from "@logtape/logtape";
|
|
88
|
+
* import {
|
|
89
|
+
* createHmacPseudonymizer,
|
|
90
|
+
* redactByFieldAsync,
|
|
91
|
+
* } from "@logtape/redaction";
|
|
92
|
+
*
|
|
93
|
+
* const pseudonymize = await createHmacPseudonymizer({ key: "secret" });
|
|
94
|
+
* const sink = redactByFieldAsync(getConsoleSink(), {
|
|
95
|
+
* fieldPatterns: [/userId/i, /email/i],
|
|
96
|
+
* action: pseudonymize,
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* @param sink The sink to wrap.
|
|
101
|
+
* @param options The async redaction options.
|
|
102
|
+
* @returns The wrapped sink.
|
|
103
|
+
* @since 2.1.0
|
|
104
|
+
*/
|
|
105
|
+
function redactByFieldAsync(sink, options) {
|
|
106
|
+
let lastPromise = Promise.resolve();
|
|
107
|
+
let closed = false;
|
|
108
|
+
const sinkErrors = [];
|
|
109
|
+
const wrapped = (record) => {
|
|
110
|
+
if (closed) return;
|
|
111
|
+
const work = redactLogRecordAsync(record, options).catch((error) => {
|
|
112
|
+
reportRedactionFailure(error);
|
|
113
|
+
return null;
|
|
114
|
+
});
|
|
115
|
+
lastPromise = lastPromise.then(async () => {
|
|
116
|
+
const result = await work;
|
|
117
|
+
if (result == null) return;
|
|
118
|
+
try {
|
|
119
|
+
await sink({
|
|
120
|
+
...record,
|
|
121
|
+
message: result.message,
|
|
122
|
+
properties: result.properties
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
sinkErrors.push(error);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
wrapped[Symbol.asyncDispose] = async () => {
|
|
130
|
+
closed = true;
|
|
131
|
+
await lastPromise;
|
|
132
|
+
let disposeError;
|
|
133
|
+
try {
|
|
134
|
+
if (Symbol.asyncDispose in sink) await sink[Symbol.asyncDispose]();
|
|
135
|
+
else if (Symbol.dispose in sink) sink[Symbol.dispose]();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
disposeError = error;
|
|
138
|
+
}
|
|
139
|
+
if (sinkErrors.length > 0) {
|
|
140
|
+
const errors = disposeError == null ? sinkErrors : [...sinkErrors, disposeError];
|
|
141
|
+
if (errors.length === 1) throw errors[0];
|
|
142
|
+
throw new AggregateError(errors, "One or more errors occurred while emitting redacted log records.");
|
|
143
|
+
}
|
|
144
|
+
if (disposeError != null) throw disposeError;
|
|
145
|
+
};
|
|
146
|
+
return wrapped;
|
|
147
|
+
}
|
|
148
|
+
async function redactLogRecordAsync(record, options) {
|
|
149
|
+
const redactedProperties = await redactPropertiesAsync(record.properties, options);
|
|
150
|
+
if (typeof record.rawMessage === "string") {
|
|
151
|
+
const placeholders = extractPlaceholderNames(record.rawMessage);
|
|
152
|
+
const { redactedIndices, wildcardIndices } = getRedactedPlaceholderIndices(placeholders, options.fieldPatterns);
|
|
153
|
+
return {
|
|
154
|
+
message: await redactMessageArrayAsync(record.message, placeholders, redactedIndices, wildcardIndices, redactedProperties, options.action),
|
|
155
|
+
properties: redactedProperties
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
let redactedMessage = record.message;
|
|
159
|
+
const redactedValues = getRedactedValues(record.properties, redactedProperties);
|
|
160
|
+
if (redactedValues.size > 0) redactedMessage = redactMessageByValues(record.message, redactedValues);
|
|
161
|
+
return {
|
|
162
|
+
message: redactedMessage,
|
|
163
|
+
properties: redactedProperties
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function reportRedactionFailure(error) {
|
|
167
|
+
if (reportingRedactionFailure || typeof metaLogger.warn !== "function") return;
|
|
168
|
+
try {
|
|
169
|
+
reportingRedactionFailure = true;
|
|
170
|
+
metaLogger.warn("Failed to redact a log record; dropping the record to avoid leaking sensitive data: {error}", { error });
|
|
171
|
+
} catch {} finally {
|
|
172
|
+
reportingRedactionFailure = false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Creates an asynchronous pseudonymizer based on HMAC using the Web Crypto API.
|
|
177
|
+
*
|
|
178
|
+
* The returned function converts each value with `String(value)`, encodes it
|
|
179
|
+
* as UTF-8, and returns a stable pseudonym. Because HMAC is keyed, this is
|
|
180
|
+
* safer than a plain salted hash for values with small input spaces, such as
|
|
181
|
+
* email addresses or numeric user IDs.
|
|
182
|
+
*
|
|
183
|
+
* @param options The HMAC pseudonymizer options.
|
|
184
|
+
* @returns An async redaction action.
|
|
185
|
+
* @since 2.1.0
|
|
186
|
+
*/
|
|
187
|
+
async function createHmacPseudonymizer(options) {
|
|
188
|
+
const subtle = globalThis.crypto?.subtle;
|
|
189
|
+
if (subtle == null) throw new TypeError("The Web Crypto API is not available.");
|
|
190
|
+
const hash = getHmacHash(options.key, options.hash);
|
|
191
|
+
const encoding = options.encoding ?? "base64url";
|
|
192
|
+
const prefix = options.prefix ?? `hmac-${hash.toLowerCase().replaceAll("-", "")}:`;
|
|
193
|
+
const key = isCryptoKey(options.key) ? options.key : await subtle.importKey("raw", keyToBytes(options.key), {
|
|
194
|
+
name: "HMAC",
|
|
195
|
+
hash
|
|
196
|
+
}, false, ["sign"]);
|
|
197
|
+
const encoder = new TextEncoder();
|
|
198
|
+
return async (value) => {
|
|
199
|
+
const data = encoder.encode(String(value));
|
|
200
|
+
const signature = new Uint8Array(await subtle.sign("HMAC", key, data));
|
|
201
|
+
return prefix + encodeBytes(signature, encoding);
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
74
205
|
* Redacts properties from an object based on specified field patterns.
|
|
75
206
|
*
|
|
76
207
|
* This function creates a shallow copy of the input object and applies
|
|
@@ -90,7 +221,7 @@ function redactProperties(properties, options, visited = /* @__PURE__ */ new Map
|
|
|
90
221
|
if (visited.has(properties)) return visited.get(properties);
|
|
91
222
|
const copy = {};
|
|
92
223
|
visited.set(properties, copy);
|
|
93
|
-
for (const field
|
|
224
|
+
for (const field of Object.keys(properties)) {
|
|
94
225
|
if (shouldFieldRedacted(field, options.fieldPatterns)) {
|
|
95
226
|
if (typeof options.action === "function") setProperty(copy, field, options.action(properties[field]));
|
|
96
227
|
continue;
|
|
@@ -103,6 +234,36 @@ function redactProperties(properties, options, visited = /* @__PURE__ */ new Map
|
|
|
103
234
|
}
|
|
104
235
|
return copy;
|
|
105
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Redacts properties from an object using an asynchronous action.
|
|
239
|
+
* @param properties The properties to redact.
|
|
240
|
+
* @param options The async redaction options.
|
|
241
|
+
* @param visited Map of visited objects to prevent circular reference issues.
|
|
242
|
+
* @returns The redacted properties.
|
|
243
|
+
* @since 2.1.0
|
|
244
|
+
*/
|
|
245
|
+
async function redactPropertiesAsync(properties, options, visited = /* @__PURE__ */ new Map()) {
|
|
246
|
+
if (visited.has(properties)) return visited.get(properties);
|
|
247
|
+
const copy = {};
|
|
248
|
+
visited.set(properties, copy);
|
|
249
|
+
const fields = [];
|
|
250
|
+
const values = [];
|
|
251
|
+
for (const field of Object.keys(properties)) {
|
|
252
|
+
fields.push(field);
|
|
253
|
+
if (shouldFieldRedacted(field, options.fieldPatterns)) {
|
|
254
|
+
values.push(Promise.resolve(options.action(properties[field])));
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const value = properties[field];
|
|
258
|
+
if (Array.isArray(value)) values.push(redactArrayAsync(value, options, visited));
|
|
259
|
+
else if (typeof value === "object" && value !== null) if (isBuiltInObject(value)) values.push(Promise.resolve(value));
|
|
260
|
+
else values.push(redactPropertiesAsync(value, options, visited));
|
|
261
|
+
else values.push(Promise.resolve(value));
|
|
262
|
+
}
|
|
263
|
+
const redactedValues = await Promise.all(values);
|
|
264
|
+
for (let i = 0; i < fields.length; i++) setProperty(copy, fields[i], redactedValues[i]);
|
|
265
|
+
return copy;
|
|
266
|
+
}
|
|
106
267
|
function setProperty(object, field, value) {
|
|
107
268
|
if (field === "__proto__") Object.defineProperty(object, field, {
|
|
108
269
|
value,
|
|
@@ -120,14 +281,40 @@ function setProperty(object, field, value) {
|
|
|
120
281
|
* @returns A new array with redacted values.
|
|
121
282
|
*/
|
|
122
283
|
function redactArray(array, options, visited) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
284
|
+
if (visited.has(array)) return visited.get(array);
|
|
285
|
+
const copy = [];
|
|
286
|
+
copy.length = array.length;
|
|
287
|
+
visited.set(array, copy);
|
|
288
|
+
for (let i = 0; i < array.length; i++) {
|
|
289
|
+
if (!(i in array)) continue;
|
|
290
|
+
const item = array[i];
|
|
291
|
+
if (Array.isArray(item)) copy[i] = redactArray(item, options, visited);
|
|
292
|
+
else if (typeof item === "object" && item !== null) if (isBuiltInObject(item)) copy[i] = item;
|
|
293
|
+
else copy[i] = redactProperties(item, options, visited);
|
|
294
|
+
else copy[i] = item;
|
|
295
|
+
}
|
|
296
|
+
return copy;
|
|
297
|
+
}
|
|
298
|
+
async function redactArrayAsync(array, options, visited) {
|
|
299
|
+
if (visited.has(array)) return visited.get(array);
|
|
300
|
+
const copy = [];
|
|
301
|
+
copy.length = array.length;
|
|
302
|
+
visited.set(array, copy);
|
|
303
|
+
const itemPromises = [];
|
|
304
|
+
for (let i = 0; i < array.length; i++) {
|
|
305
|
+
if (!(i in array)) {
|
|
306
|
+
itemPromises.push(Promise.resolve(void 0));
|
|
307
|
+
continue;
|
|
128
308
|
}
|
|
129
|
-
|
|
130
|
-
|
|
309
|
+
const item = array[i];
|
|
310
|
+
if (Array.isArray(item)) itemPromises.push(redactArrayAsync(item, options, visited));
|
|
311
|
+
else if (typeof item === "object" && item !== null) if (isBuiltInObject(item)) itemPromises.push(Promise.resolve(item));
|
|
312
|
+
else itemPromises.push(redactPropertiesAsync(item, options, visited));
|
|
313
|
+
else itemPromises.push(Promise.resolve(item));
|
|
314
|
+
}
|
|
315
|
+
const redactedItems = await Promise.all(itemPromises);
|
|
316
|
+
for (let i = 0; i < redactedItems.length; i++) if (i in array) copy[i] = redactedItems[i];
|
|
317
|
+
return copy;
|
|
131
318
|
}
|
|
132
319
|
/**
|
|
133
320
|
* Checks if a value is a built-in object that should not be recursively
|
|
@@ -188,11 +375,51 @@ function extractPlaceholderNames(template) {
|
|
|
188
375
|
function parsePathSegments(path) {
|
|
189
376
|
const segments = [];
|
|
190
377
|
let current = "";
|
|
191
|
-
|
|
192
|
-
|
|
378
|
+
let inBracket = false;
|
|
379
|
+
let quotedBracketSegment = false;
|
|
380
|
+
let quote;
|
|
381
|
+
let escaped = false;
|
|
382
|
+
const pushCurrent = (trim = false) => {
|
|
383
|
+
const segment = trim ? current.trimEnd() : current;
|
|
384
|
+
if (segment) segments.push(segment);
|
|
193
385
|
current = "";
|
|
194
|
-
}
|
|
195
|
-
|
|
386
|
+
};
|
|
387
|
+
for (const char of path) {
|
|
388
|
+
if (quote != null) {
|
|
389
|
+
if (escaped) {
|
|
390
|
+
current += char;
|
|
391
|
+
escaped = false;
|
|
392
|
+
} else if (char === "\\") escaped = true;
|
|
393
|
+
else if (char === quote) quote = void 0;
|
|
394
|
+
else current += char;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (inBracket && current === "" && /\s/.test(char)) continue;
|
|
398
|
+
if (inBracket && (char === "\"" || char === "'") && current === "") {
|
|
399
|
+
quote = char;
|
|
400
|
+
quotedBracketSegment = true;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (inBracket && quotedBracketSegment && /\s/.test(char)) continue;
|
|
404
|
+
if (char === "." && !inBracket) {
|
|
405
|
+
pushCurrent();
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (char === "[") {
|
|
409
|
+
pushCurrent();
|
|
410
|
+
inBracket = true;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (char === "]") {
|
|
414
|
+
pushCurrent(!quotedBracketSegment);
|
|
415
|
+
inBracket = false;
|
|
416
|
+
quotedBracketSegment = false;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (char === "?") continue;
|
|
420
|
+
current += char;
|
|
421
|
+
}
|
|
422
|
+
pushCurrent();
|
|
196
423
|
return segments;
|
|
197
424
|
}
|
|
198
425
|
/**
|
|
@@ -247,14 +474,51 @@ function redactMessageArray(message, placeholders, redactedIndices, wildcardIndi
|
|
|
247
474
|
else result.push(action(message[i]));
|
|
248
475
|
else {
|
|
249
476
|
const placeholderName = placeholders[placeholderIndex];
|
|
250
|
-
const
|
|
251
|
-
if (
|
|
477
|
+
const redactedValue = getPathValue(redactedProperties, placeholderName);
|
|
478
|
+
if (redactedValue.found) result.push(redactedValue.value);
|
|
479
|
+
else result.push(message[i]);
|
|
480
|
+
}
|
|
481
|
+
placeholderIndex++;
|
|
482
|
+
}
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
async function redactMessageArrayAsync(message, placeholders, redactedIndices, wildcardIndices, redactedProperties, action) {
|
|
486
|
+
const result = [];
|
|
487
|
+
const tasks = [];
|
|
488
|
+
let placeholderIndex = 0;
|
|
489
|
+
for (let i = 0; i < message.length; i++) if (i % 2 === 0) result.push(message[i]);
|
|
490
|
+
else {
|
|
491
|
+
if (wildcardIndices.has(placeholderIndex)) result.push(redactedProperties);
|
|
492
|
+
else if (redactedIndices.has(placeholderIndex)) {
|
|
493
|
+
const index = result.length;
|
|
494
|
+
result.push(void 0);
|
|
495
|
+
tasks.push(Promise.resolve(action(message[i])).then((redacted) => {
|
|
496
|
+
result[index] = redacted;
|
|
497
|
+
}));
|
|
498
|
+
} else {
|
|
499
|
+
const placeholderName = placeholders[placeholderIndex];
|
|
500
|
+
const redactedValue = getPathValue(redactedProperties, placeholderName);
|
|
501
|
+
if (redactedValue.found) result.push(redactedValue.value);
|
|
252
502
|
else result.push(message[i]);
|
|
253
503
|
}
|
|
254
504
|
placeholderIndex++;
|
|
255
505
|
}
|
|
506
|
+
await Promise.all(tasks);
|
|
256
507
|
return result;
|
|
257
508
|
}
|
|
509
|
+
function getPathValue(properties, path) {
|
|
510
|
+
const segments = parsePathSegments(path);
|
|
511
|
+
if (segments.length < 1) return { found: false };
|
|
512
|
+
let value = properties;
|
|
513
|
+
for (const segment of segments) {
|
|
514
|
+
if (typeof value !== "object" && typeof value !== "function" || value == null || !Object.hasOwn(value, segment)) return { found: false };
|
|
515
|
+
value = value[segment];
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
found: true,
|
|
519
|
+
value
|
|
520
|
+
};
|
|
521
|
+
}
|
|
258
522
|
/**
|
|
259
523
|
* Collects redacted value mappings from original to redacted properties.
|
|
260
524
|
* @param original The original properties.
|
|
@@ -262,7 +526,7 @@ function redactMessageArray(message, placeholders, redactedIndices, wildcardIndi
|
|
|
262
526
|
* @param map The map to populate with original -> redacted value pairs.
|
|
263
527
|
*/
|
|
264
528
|
function collectRedactedValues(original, redacted, map) {
|
|
265
|
-
for (const key
|
|
529
|
+
for (const key of Object.keys(original)) {
|
|
266
530
|
const origVal = original[key];
|
|
267
531
|
const redVal = redacted[key];
|
|
268
532
|
if (origVal !== redVal) map.set(origVal, redVal);
|
|
@@ -298,7 +562,53 @@ function redactMessageByValues(message, redactedValues) {
|
|
|
298
562
|
}
|
|
299
563
|
return result;
|
|
300
564
|
}
|
|
565
|
+
function keyToBytes(key) {
|
|
566
|
+
if (typeof key === "string") return new TextEncoder().encode(key);
|
|
567
|
+
if (key instanceof ArrayBuffer) return key;
|
|
568
|
+
return key;
|
|
569
|
+
}
|
|
570
|
+
function isCryptoKey(key) {
|
|
571
|
+
return typeof key === "object" && key !== null && Object.prototype.toString.call(key) === "[object CryptoKey]";
|
|
572
|
+
}
|
|
573
|
+
function getHmacHash(key, hash) {
|
|
574
|
+
if (!isCryptoKey(key)) return hash ?? "SHA-256";
|
|
575
|
+
if (!key.usages.includes("sign")) throw new TypeError("The HMAC CryptoKey must include the \"sign\" usage.");
|
|
576
|
+
const keyHash = getCryptoKeyHmacHash(key);
|
|
577
|
+
if (hash != null && hash !== keyHash) throw new TypeError(`The HMAC CryptoKey uses ${keyHash}, but the "hash" option is ${hash}.`);
|
|
578
|
+
return keyHash;
|
|
579
|
+
}
|
|
580
|
+
function getCryptoKeyHmacHash(key) {
|
|
581
|
+
const algorithm = key.algorithm;
|
|
582
|
+
if (algorithm.name !== "HMAC" || !("hash" in algorithm)) throw new TypeError("The CryptoKey must be an HMAC key.");
|
|
583
|
+
const hashAlgorithm = algorithm.hash;
|
|
584
|
+
if (typeof hashAlgorithm !== "object" || hashAlgorithm == null || !("name" in hashAlgorithm) || typeof hashAlgorithm.name !== "string") throw new TypeError("The CryptoKey must specify an HMAC hash algorithm.");
|
|
585
|
+
const hash = hashAlgorithm.name;
|
|
586
|
+
switch (hash) {
|
|
587
|
+
case "SHA-256":
|
|
588
|
+
case "SHA-384":
|
|
589
|
+
case "SHA-512": return hash;
|
|
590
|
+
default: throw new TypeError(`Unsupported HMAC hash algorithm: ${hash}.`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function encodeBytes(bytes, encoding) {
|
|
594
|
+
switch (encoding) {
|
|
595
|
+
case "base64url": return encodeBase64Url(bytes);
|
|
596
|
+
case "hex": return encodeHex(bytes);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function encodeBase64Url(bytes) {
|
|
600
|
+
let binary = "";
|
|
601
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
602
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
603
|
+
}
|
|
604
|
+
function encodeHex(bytes) {
|
|
605
|
+
let result = "";
|
|
606
|
+
for (const byte of bytes) result += byte.toString(16).padStart(2, "0");
|
|
607
|
+
return result;
|
|
608
|
+
}
|
|
301
609
|
|
|
302
610
|
//#endregion
|
|
303
611
|
exports.DEFAULT_REDACT_FIELDS = DEFAULT_REDACT_FIELDS;
|
|
304
|
-
exports.
|
|
612
|
+
exports.createHmacPseudonymizer = createHmacPseudonymizer;
|
|
613
|
+
exports.redactByField = redactByField;
|
|
614
|
+
exports.redactByFieldAsync = redactByFieldAsync;
|
package/dist/field.d.cts
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.cts.map
|
package/dist/field.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field.d.cts","names":[],"sources":["../src/field.ts"],"sourcesContent":[],"mappings":";;;;;;AAOA;AAOA;
|
|
1
|
+
{"version":3,"file":"field.d.cts","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"}
|