@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/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?: "delete" | ((value: unknown) => unknown);
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
@@ -1 +1 @@
1
- {"version":3,"file":"field.d.ts","names":[],"sources":["../src/field.ts"],"sourcesContent":[],"mappings":";;;;;;AAOA;AAOA;AAQA;AAqBiB,KApCL,YAAA,GAoCK,MAAqB,GApCF,MA2CV;AAyC1B;;;;;AACmC,KA9EvB,aAAA,GAAgB,YA8EO,EAAA;;;;;;;AAEL,cAxEjB,qBAwEiB,EAxEM,aAwEN;;AAAsB;;;;UAnDnC,qBAAA;;;;;;;0BAOS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyCV,aAAA,OACR,OAAO,OAAO,aAAa,OAAO,2BAC/B,wBAAwB,gBAChC,OAAO,OAAO,aAAa,OAAO"}
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 in properties) {
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
- return array.map((item) => {
123
- if (Array.isArray(item)) return redactArray(item, options, visited);
124
- if (typeof item === "object" && item !== null) {
125
- if (isBuiltInObject(item)) return item;
126
- return redactProperties(item, options, visited);
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
- return item;
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
- for (const char of path) if (char === "." || char === "[") {
191
- if (current) segments.push(current);
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
- } else if (char === "]" || char === "?") {} else current += char;
194
- if (current) segments.push(current);
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 rootKey = parsePathSegments(placeholderName)[0];
250
- if (rootKey in redactedProperties) result.push(redactedProperties[rootKey]);
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 in original) {
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