@ogcio/o11y-sdk-node 0.3.0 → 0.4.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/lib/exporter/pii-exporter-decorator.d.ts +3 -1
  3. package/dist/lib/exporter/pii-exporter-decorator.js +33 -17
  4. package/dist/lib/index.d.ts +5 -0
  5. package/dist/lib/instrumentation.node.js +0 -8
  6. package/dist/lib/internals/redaction/pii-detection.d.ts +23 -0
  7. package/dist/lib/internals/redaction/pii-detection.js +87 -0
  8. package/dist/lib/internals/redaction/redactors/email.d.ts +8 -0
  9. package/dist/lib/internals/redaction/redactors/email.js +48 -0
  10. package/dist/lib/internals/redaction/redactors/index.d.ts +4 -0
  11. package/dist/lib/internals/redaction/redactors/index.js +6 -0
  12. package/dist/lib/internals/redaction/redactors/ip.d.ts +10 -0
  13. package/dist/lib/internals/redaction/redactors/ip.js +54 -0
  14. package/dist/lib/processor/enrich-logger-processor.d.ts +2 -2
  15. package/dist/package.json +14 -14
  16. package/dist/vitest.config.js +4 -4
  17. package/lib/exporter/pii-exporter-decorator.ts +56 -24
  18. package/lib/index.ts +5 -0
  19. package/lib/instrumentation.node.ts +0 -10
  20. package/lib/internals/redaction/pii-detection.ts +126 -0
  21. package/lib/internals/redaction/redactors/email.ts +58 -0
  22. package/lib/internals/redaction/redactors/index.ts +12 -0
  23. package/lib/internals/redaction/redactors/ip.ts +68 -0
  24. package/lib/internals/shared-metrics.ts +1 -1
  25. package/lib/processor/enrich-logger-processor.ts +2 -2
  26. package/package.json +14 -14
  27. package/test/exporter/pii-exporter-decorator.test.ts +75 -126
  28. package/test/internals/pii-detection.test.ts +27 -25
  29. package/test/internals/redactors/email.test.ts +81 -0
  30. package/test/internals/redactors/ip.test.ts +89 -0
  31. package/test/traces/active-span.test.ts +1 -1
  32. package/vitest.config.ts +4 -4
  33. package/dist/lib/internals/pii-detection.d.ts +0 -17
  34. package/dist/lib/internals/pii-detection.js +0 -116
  35. package/lib/internals/pii-detection.ts +0 -145
@@ -1,17 +1,22 @@
1
1
  import { ExportResult } from "@opentelemetry/core";
2
2
  import { OTLPExporterBase } from "@opentelemetry/otlp-exporter-base";
3
3
  import { ReadableLogRecord } from "@opentelemetry/sdk-logs";
4
+ import {
5
+ PushMetricExporter,
6
+ ResourceMetrics,
7
+ } from "@opentelemetry/sdk-metrics";
4
8
  import { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
5
9
  import { NodeSDKConfig } from "../index.js";
6
10
  import {
7
11
  _cleanLogBodyPII,
8
12
  _cleanObjectPII,
9
13
  _cleanStringPII,
10
- } from "../internals/pii-detection.js";
14
+ } from "../internals/redaction/pii-detection.js";
11
15
  import {
12
- PushMetricExporter,
13
- ResourceMetrics,
14
- } from "@opentelemetry/sdk-metrics";
16
+ Redactor,
17
+ RedactorKeys,
18
+ redactors,
19
+ } from "../internals/redaction/redactors/index.js";
15
20
 
16
21
  export class PIIExporterDecorator
17
22
  extends OTLPExporterBase<
@@ -21,6 +26,7 @@ export class PIIExporterDecorator
21
26
  {
22
27
  private readonly _exporter;
23
28
  private readonly _config;
29
+ private readonly _redactors;
24
30
 
25
31
  constructor(
26
32
  exporter: OTLPExporterBase<
@@ -31,6 +37,7 @@ export class PIIExporterDecorator
31
37
  super(exporter["_delegate"]);
32
38
  this._exporter = exporter;
33
39
  this._config = config;
40
+ this._redactors = this._buildRedactors(config.detection);
34
41
  }
35
42
 
36
43
  forceFlush(): Promise<void> {
@@ -45,7 +52,7 @@ export class PIIExporterDecorator
45
52
  items: (ReadableSpan | ReadableLogRecord)[] | ResourceMetrics,
46
53
  resultCallback: (result: ExportResult) => void,
47
54
  ): void {
48
- if (!this._config.detection?.email) {
55
+ if (this._redactors.length === 0) {
49
56
  this._exporter.export(items, resultCallback);
50
57
  return;
51
58
  }
@@ -55,7 +62,7 @@ export class PIIExporterDecorator
55
62
  if (this._isReadableSpan(item)) {
56
63
  this._redactSpan(item);
57
64
  } else if (this._isReadableLogRecord(item)) {
58
- this._redactLogRecord(item);
65
+ item = this._redactLogRecord(item);
59
66
  }
60
67
  return item;
61
68
  });
@@ -102,51 +109,76 @@ export class PIIExporterDecorator
102
109
  );
103
110
  }
104
111
 
105
- private _redactSpan(span: ReadableSpan) {
112
+ private _redactSpan(span: ReadableSpan): void {
106
113
  Object.assign(span, {
107
- name: _cleanStringPII(span.name, "trace"),
108
- attributes: span.attributes && _cleanObjectPII(span.attributes, "trace"),
114
+ name: _cleanStringPII(span.name, "trace", this._redactors),
115
+ attributes:
116
+ span.attributes &&
117
+ _cleanObjectPII(span.attributes, "trace", this._redactors),
109
118
  resource: {
110
119
  attributes:
111
120
  span?.resource?.attributes &&
112
- _cleanObjectPII(span.resource.attributes, "trace"),
121
+ _cleanObjectPII(span.resource.attributes, "trace", this._redactors),
113
122
  },
114
123
  links: span?.links?.map((link) => {
115
124
  Object.assign(link, {
116
125
  attributes:
117
- link?.attributes && _cleanObjectPII(link.attributes, "trace"),
126
+ link?.attributes &&
127
+ _cleanObjectPII(link.attributes, "trace", this._redactors),
118
128
  });
119
129
  }),
120
130
  events: span?.events?.map((event) => {
121
131
  Object.assign(event, {
122
- name: _cleanStringPII(event.name, "trace"),
132
+ name: _cleanStringPII(event.name, "trace", this._redactors),
123
133
  attributes:
124
- event?.attributes && _cleanObjectPII(event.attributes, "trace"),
134
+ event?.attributes &&
135
+ _cleanObjectPII(event.attributes, "trace", this._redactors),
125
136
  });
126
137
  return event;
127
138
  }),
128
139
  });
129
140
  }
130
141
 
131
- private _redactLogRecord(log: ReadableLogRecord) {
132
- Object.assign(log, {
133
- body: _cleanLogBodyPII(log.body),
134
- attributes: log.attributes && _cleanObjectPII(log.attributes, "log"),
135
- resource: {
136
- attributes:
137
- log?.resource?.attributes &&
138
- _cleanObjectPII(log.resource.attributes, "log"),
142
+ private _redactLogRecord(log: ReadableLogRecord): ReadableLogRecord {
143
+ return {
144
+ ...log,
145
+ body: _cleanLogBodyPII(log.body, this._redactors),
146
+ attributes:
147
+ log.attributes &&
148
+ _cleanObjectPII(log.attributes, "log", this._redactors),
149
+ resource: log.resource && {
150
+ ...log.resource,
151
+ attributes: _cleanObjectPII(
152
+ log.resource.attributes,
153
+ "log",
154
+ this._redactors,
155
+ ),
139
156
  },
140
- });
157
+ };
141
158
  }
142
159
 
143
- private _redactResourceMetrics(metric: ResourceMetrics) {
160
+ private _redactResourceMetrics(metric: ResourceMetrics): void {
144
161
  Object.assign(metric, {
145
162
  resource: {
146
163
  attributes:
147
164
  metric?.resource?.attributes &&
148
- _cleanObjectPII(metric.resource.attributes, "metric"),
165
+ _cleanObjectPII(
166
+ metric.resource.attributes,
167
+ "metric",
168
+ this._redactors,
169
+ ),
149
170
  },
150
171
  });
151
172
  }
173
+
174
+ // Default opt-in every redactor available, excluding only those explicitly configured to false
175
+ private _buildRedactors(
176
+ redactorsConfig: NodeSDKConfig["detection"] = {},
177
+ ): Redactor[] {
178
+ return Object.entries(redactors)
179
+ .filter(([key]) => {
180
+ return redactorsConfig[key as RedactorKeys] !== false;
181
+ })
182
+ .map(([_, value]) => value);
183
+ }
152
184
  }
package/lib/index.ts CHANGED
@@ -89,6 +89,11 @@ export interface NodeSDKConfig {
89
89
  * @default true
90
90
  */
91
91
  email?: boolean;
92
+ /**
93
+ * Redact IPv4/IPv6 addresses
94
+ * @default true
95
+ */
96
+ ip?: boolean;
92
97
  };
93
98
  }
94
99
 
@@ -41,16 +41,6 @@ export default async function buildNodeInstrumentation(
41
41
  return;
42
42
  }
43
43
 
44
- if (!config.detection) {
45
- config.detection = {
46
- email: true,
47
- };
48
- }
49
-
50
- if (config.detection.email === undefined) {
51
- config.detection.email = true;
52
- }
53
-
54
44
  // Init configManager to make it available to all o11y utils.
55
45
  setNodeSdkConfig(config);
56
46
 
@@ -0,0 +1,126 @@
1
+ import type { AnyValue, AnyValueMap } from "@opentelemetry/api-logs";
2
+ import { Redactor } from "./redactors/index.js";
3
+ import { AttributeValue } from "@opentelemetry/api";
4
+
5
+ const decoder = new TextDecoder();
6
+ const encoder = new TextEncoder();
7
+
8
+ export type PIISource = "trace" | "log" | "metric";
9
+
10
+ /**
11
+ * Checks whether a string contains URI-encoded components.
12
+ *
13
+ * @param {string} value - The string to inspect.
14
+ * @returns {boolean} `true` if the string is encoded, `false` otherwise.
15
+ */
16
+ function _containsEncodedComponents(value: string): boolean {
17
+ try {
18
+ return decodeURI(value) !== decodeURIComponent(value);
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
26
+ *
27
+ * If the string is URL-encoded, it will be decoded before redaction.
28
+ * Metrics are emitted for:
29
+ * - each domain found in redacted email addresses.
30
+ * - IPv4|IPv6 addresses redacted.
31
+ *
32
+ * @template T
33
+ *
34
+ * @param {T} value - The input value to sanitize.
35
+ * @param {"trace" | "log"} source - The source context of the input, used in metrics.
36
+ * @param {Redactor[]} redactors - The string processors containing the redaction logic.
37
+ *
38
+ * @returns {T} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
39
+ */
40
+ export function _cleanStringPII<T extends AnyValue>(
41
+ value: T,
42
+ source: PIISource,
43
+ redactors: Redactor[],
44
+ ): T {
45
+ if (Array.isArray(value)) {
46
+ return value.map((v) =>
47
+ _cleanStringPII<typeof v>(v, source, redactors),
48
+ ) as T;
49
+ }
50
+
51
+ if (typeof value !== "string") {
52
+ return value;
53
+ }
54
+
55
+ let kind: "string" | "url" = "string";
56
+ let decodedValue: string = value;
57
+
58
+ if (_containsEncodedComponents(value)) {
59
+ decodedValue = decodeURIComponent(value);
60
+ kind = "url";
61
+ }
62
+
63
+ return redactors.reduce(
64
+ (redactedValue: string, currentRedactor): string =>
65
+ currentRedactor(redactedValue, source, kind),
66
+ decodedValue,
67
+ ) as T;
68
+ }
69
+
70
+ export function _cleanObjectPII(
71
+ entry: object,
72
+ source: PIISource,
73
+ redactors: Redactor[],
74
+ ): Record<string, AttributeValue> {
75
+ if (!entry) {
76
+ return entry;
77
+ }
78
+
79
+ return Object.fromEntries(
80
+ Object.entries(entry).map(([k, v]) => [
81
+ k,
82
+ _cleanStringPII(v, source, redactors),
83
+ ]),
84
+ );
85
+ }
86
+
87
+ export function _cleanLogBodyPII<T extends AnyValue>(
88
+ value: T,
89
+ redactors: Redactor[],
90
+ ): T {
91
+ if (typeof value === "string") {
92
+ return _cleanStringPII(value, "log", redactors);
93
+ }
94
+
95
+ if (
96
+ typeof value === "number" ||
97
+ typeof value === "boolean" ||
98
+ value == null
99
+ ) {
100
+ return value;
101
+ }
102
+
103
+ if (value instanceof Uint8Array) {
104
+ try {
105
+ const decoded = decoder.decode(value);
106
+ const sanitized = _cleanStringPII(decoded, "log", redactors);
107
+ return encoder.encode(sanitized) as T;
108
+ } catch {
109
+ return value;
110
+ }
111
+ }
112
+
113
+ if (Array.isArray(value)) {
114
+ return value.map((value) => _cleanLogBodyPII(value, redactors)) as T;
115
+ }
116
+
117
+ if (typeof value === "object") {
118
+ const sanitized: AnyValueMap = {};
119
+ for (const [key, val] of Object.entries(value)) {
120
+ sanitized[key] = _cleanLogBodyPII(val, redactors);
121
+ }
122
+ return sanitized as T;
123
+ }
124
+
125
+ return value;
126
+ }
@@ -0,0 +1,58 @@
1
+ import { _getPIICounterRedactionMetric } from "../../shared-metrics.js";
2
+
3
+ const EMAIL_REGEX = /[\p{L}\p{N}._%+-]+@((?:[\p{L}\p{N}-]+\.)+[\p{L}]{2,})/giu;
4
+
5
+ /**
6
+ * Redacts all email addresses in the input string and collects metadata.
7
+ *
8
+ * @param {string} value The input string potentially containing email addresses.
9
+ * @returns {{
10
+ * redacted: string,
11
+ * count: number,
12
+ * domains: Record<string, number>
13
+ * }}
14
+ *
15
+ * An object containing:
16
+ * - `redacted`: the string with email addresses replaced by `[REDACTED EMAIL]`
17
+ * - `count`: total number of email addresses redacted
18
+ * - `domains`: a map of domain names to the number of times they were redacted
19
+ */
20
+ function _redactEmails(value: string): {
21
+ redacted: string;
22
+ count: number;
23
+ domains: Record<string, number>;
24
+ } {
25
+ let count = 0;
26
+ const domains: Record<string, number> = {};
27
+
28
+ const redacted = value.replace(EMAIL_REGEX, (_, domain) => {
29
+ count++;
30
+ domains[domain] = (domains[domain] || 0) + 1;
31
+ return "[REDACTED EMAIL]";
32
+ });
33
+
34
+ return { redacted, count, domains };
35
+ }
36
+
37
+ /**
38
+ * Redacts provided input and collects metadata metrics about redacted email domains,
39
+ * data source and kind.
40
+ *
41
+ * @param {string} value The input string potentially containing email addresses.
42
+ * @returns {string} the redacted value
43
+ */
44
+ export const emailRedactor = (value: string, source: string, kind: string) => {
45
+ const { redacted, count, domains } = _redactEmails(value);
46
+
47
+ if (count > 0) {
48
+ for (const [domain, domainCount] of Object.entries(domains)) {
49
+ _getPIICounterRedactionMetric().add(domainCount, {
50
+ pii_type: "email",
51
+ redaction_source: source,
52
+ pii_email_domain: domain,
53
+ pii_format: kind,
54
+ });
55
+ }
56
+ }
57
+ return redacted;
58
+ };
@@ -0,0 +1,12 @@
1
+ import { NodeSDKConfig } from "../../../index.js";
2
+ import { emailRedactor } from "./email.js";
3
+ import { ipRedactor } from "./ip.js";
4
+
5
+ export type Redactor = (value: string, source: string, kind: string) => string;
6
+
7
+ export type RedactorKeys = keyof NonNullable<NodeSDKConfig["detection"]>;
8
+
9
+ export const redactors: Record<RedactorKeys, Redactor> = {
10
+ email: emailRedactor,
11
+ ip: ipRedactor,
12
+ };
@@ -0,0 +1,68 @@
1
+ import { _getPIICounterRedactionMetric } from "../../shared-metrics.js";
2
+
3
+ // Generous IP address matchers (might match some invalid addresses like 192.168.01.1)
4
+ const IPV4_REGEX =
5
+ /(?<!\d)(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}(?!\d)/gi;
6
+ const IPV6_REGEX =
7
+ /(?<![0-9a-f:])((?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|:(?::[0-9A-Fa-f]{1,4}){1,7}|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?::[0-9A-Fa-f]{1,4}){1,6}|:(?::[0-9A-Fa-f]{1,4}){1,7}:?|(?:[0-9A-Fa-f]{1,4}:){1,4}:(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})){3})(?![0-9a-f:])/gi;
8
+
9
+ /**
10
+ * Redacts all ip addresses in the input string and collects metadata.
11
+ *
12
+ * @param {string} value The input string potentially containing ip addresses.
13
+ * @returns {{
14
+ * redacted: string,
15
+ * count: number,
16
+ * domains: Record<string, number>
17
+ * }}
18
+ *
19
+ * An object containing:
20
+ * - `redacted`: the string with IP addresses replaced by `[REDACTED IPV*]`
21
+ * - `counters`: total number of addresses redacted by IPv* type
22
+ * - `domains`: a map of domain names to the number of times they were redacted
23
+ */
24
+ function _redactIps(value: string): {
25
+ redacted: string;
26
+ counters: Record<string, number>;
27
+ } {
28
+ const counters: Record<string, number> = {};
29
+ const redacted = value
30
+ .replace(IPV4_REGEX, () => {
31
+ counters["IPv4"] = (counters["IPv4"] || 0) + 1;
32
+ return "[REDACTED IPV4]";
33
+ })
34
+ .replace(IPV6_REGEX, () => {
35
+ counters["IPv4"] = (counters["IPv4"] || 0) + 1;
36
+ return "[REDACTED IPV6]";
37
+ });
38
+ return { redacted, counters };
39
+ }
40
+
41
+ /**
42
+ * Redacts provided input and collects metadata metrics about redacted IPs,
43
+ * data source and kind.
44
+ *
45
+ * @param {string} value The input string potentially containing IP addresses.
46
+ * @param {string} source The source of the attribute being redacted (log, span, metric).
47
+ * @param {string} kind The type of the data structure containing the PII
48
+ * @returns {string} the redacted value
49
+ */
50
+ export const ipRedactor = (
51
+ value: string,
52
+ source: string,
53
+ kind: string,
54
+ ): string => {
55
+ const { redacted, counters } = _redactIps(value);
56
+
57
+ Object.entries(counters).forEach(([type, counter]) => {
58
+ if (counter > 0) {
59
+ _getPIICounterRedactionMetric().add(counter, {
60
+ pii_type: type,
61
+ redaction_source: source,
62
+ pii_format: kind,
63
+ });
64
+ }
65
+ });
66
+
67
+ return redacted;
68
+ };
@@ -1,6 +1,6 @@
1
1
  import { Attributes, Counter } from "@opentelemetry/api";
2
2
  import { getMetric } from "../metrics.js";
3
- import { PIISource } from "./pii-detection.js";
3
+ import { PIISource } from "./redaction/pii-detection.js";
4
4
 
5
5
  interface RedactionMetric extends Attributes {
6
6
  /** Type of PII redacted (e.g., "email", "phone"). */
@@ -1,4 +1,4 @@
1
- import { LogRecord, LogRecordProcessor } from "@opentelemetry/sdk-logs";
1
+ import { SdkLogRecord, LogRecordProcessor } from "@opentelemetry/sdk-logs";
2
2
  import { Context } from "@opentelemetry/api";
3
3
  import { SignalAttributeValue } from "../index.js";
4
4
 
@@ -18,7 +18,7 @@ export class EnrichLogProcessor implements LogRecordProcessor {
18
18
  forceFlush(): Promise<void> {
19
19
  return Promise.resolve();
20
20
  }
21
- onEmit(logRecord: LogRecord, _context?: Context): void {
21
+ onEmit(logRecord: SdkLogRecord, _context?: Context): void {
22
22
  if (this._spanAttributes) {
23
23
  for (const [key, value] of Object.entries(this._spanAttributes)) {
24
24
  logRecord.setAttribute(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-node",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -22,26 +22,26 @@
22
22
  "@grpc/grpc-js": "^1.13.4",
23
23
  "@opentelemetry/api": "^1.9.0",
24
24
  "@opentelemetry/api-logs": "^0.203.0",
25
- "@opentelemetry/auto-instrumentations-node": "^0.60.1",
25
+ "@opentelemetry/auto-instrumentations-node": "^0.62.1",
26
26
  "@opentelemetry/core": "^2.0.1",
27
- "@opentelemetry/exporter-logs-otlp-grpc": "^0.202.0",
28
- "@opentelemetry/exporter-logs-otlp-http": "^0.202.0",
29
- "@opentelemetry/exporter-metrics-otlp-grpc": "^0.202.0",
30
- "@opentelemetry/exporter-metrics-otlp-http": "^0.202.0",
31
- "@opentelemetry/exporter-trace-otlp-grpc": "^0.202.0",
32
- "@opentelemetry/exporter-trace-otlp-http": "^0.202.0",
33
- "@opentelemetry/instrumentation": "^0.202.0",
34
- "@opentelemetry/otlp-exporter-base": "^0.202.0",
27
+ "@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0",
28
+ "@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
29
+ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.203.0",
30
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.203.0",
31
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0",
32
+ "@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
33
+ "@opentelemetry/instrumentation": "^0.203.0",
34
+ "@opentelemetry/otlp-exporter-base": "^0.203.0",
35
35
  "@opentelemetry/resources": "^2.0.1",
36
- "@opentelemetry/sdk-logs": "^0.202.0",
36
+ "@opentelemetry/sdk-logs": "^0.203.0",
37
37
  "@opentelemetry/sdk-metrics": "^2.0.1",
38
- "@opentelemetry/sdk-node": "^0.202.0",
38
+ "@opentelemetry/sdk-node": "^0.203.0",
39
39
  "@opentelemetry/sdk-trace-base": "^2.0.1"
40
40
  },
41
41
  "devDependencies": {
42
- "@types/node": "^24.0.10",
42
+ "@types/node": "^24.3.0",
43
43
  "@vitest/coverage-v8": "^3.2.4",
44
- "tsx": "^4.20.3",
44
+ "tsx": "^4.20.5",
45
45
  "typescript": "^5.8.3",
46
46
  "vitest": "^3.2.4"
47
47
  },