autotel 4.1.0 → 4.2.1
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/auto.cjs +5 -3
- package/dist/auto.cjs.map +1 -1
- package/dist/auto.js +3 -3
- package/dist/auto.js.map +1 -1
- package/dist/chunk-C_NdSu1c.cjs +34 -0
- package/dist/correlation-id.cjs +1 -1
- package/dist/correlation-id.d.cts.map +1 -1
- package/dist/correlation-id.d.ts.map +1 -1
- package/dist/correlation-id.js +1 -1
- package/dist/decorators.cjs +1 -1
- package/dist/decorators.js +1 -1
- package/dist/{event-ByBTV9M2.js → event-531asIM6.js} +4 -4
- package/dist/{event-ByBTV9M2.js.map → event-531asIM6.js.map} +1 -1
- package/dist/{event-BhHREDJk.cjs → event-CcZYwp50.cjs} +4 -4
- package/dist/{event-BhHREDJk.cjs.map → event-CcZYwp50.cjs.map} +1 -1
- package/dist/event.cjs +1 -1
- package/dist/event.js +1 -1
- package/dist/{functional-zpzNLhky.cjs → functional-C8B0Qa7o.cjs} +10 -7
- package/dist/functional-C8B0Qa7o.cjs.map +1 -0
- package/dist/{functional-DtI0u4vx.js → functional-r-AUIRy_.js} +9 -9
- package/dist/functional-r-AUIRy_.js.map +1 -0
- package/dist/functional.cjs +1 -1
- package/dist/functional.js +1 -1
- package/dist/http.cjs +1 -1
- package/dist/http.js +1 -1
- package/dist/index.cjs +15 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -14
- package/dist/index.js.map +1 -1
- package/dist/{init-D-jnNMix.js → init-BS2JVkrL.js} +2 -2
- package/dist/{init-D-jnNMix.js.map → init-BS2JVkrL.js.map} +1 -1
- package/dist/{init-BX7AmFRl.cjs → init-BXiuPK6j.cjs} +3 -3
- package/dist/{init-BX7AmFRl.cjs.map → init-BXiuPK6j.cjs.map} +1 -1
- package/dist/instrumentation.cjs +2 -2
- package/dist/instrumentation.js +2 -2
- package/dist/logger.cjs +236 -8
- package/dist/logger.cjs.map +1 -0
- package/dist/messaging.cjs +1 -1
- package/dist/messaging.js +1 -1
- package/dist/{node-require-DF5QBX6z.cjs → node-require-CZ_PU448.cjs} +6 -4
- package/dist/node-require-CZ_PU448.cjs.map +1 -0
- package/dist/{node-require-Db1oDpLj.js → node-require-vROmTeJ8.js} +5 -5
- package/dist/node-require-vROmTeJ8.js.map +1 -0
- package/dist/{operation-context-C-2hmmtP.js → operation-context-CKBoA4Qy.js} +3 -3
- package/dist/operation-context-CKBoA4Qy.js.map +1 -0
- package/dist/{operation-context-n4_obUwq.cjs → operation-context-D6LDf4W_.cjs} +3 -1
- package/dist/operation-context-D6LDf4W_.cjs.map +1 -0
- package/dist/register.cjs +3 -1
- package/dist/register.cjs.map +1 -1
- package/dist/register.js +2 -2
- package/dist/register.js.map +1 -1
- package/dist/semantic-helpers.cjs +1 -1
- package/dist/semantic-helpers.js +1 -1
- package/dist/{stable-hash-Cg5cT34Q.js → stable-hash-ChFBIhNt.js} +3 -3
- package/dist/stable-hash-ChFBIhNt.js.map +1 -0
- package/dist/{stable-hash-BNTMrmdB.cjs → stable-hash-brKISGf1.cjs} +4 -2
- package/dist/stable-hash-brKISGf1.cjs.map +1 -0
- package/dist/trace-context-Cijqoi6e.d.cts.map +1 -1
- package/dist/trace-context-Cijqoi6e.d.ts.map +1 -1
- package/dist/trace-helpers.cjs +1 -1
- package/dist/trace-helpers.js +1 -1
- package/dist/{track-wc0HafS_.js → track-COUuU48p.js} +5 -5
- package/dist/track-COUuU48p.js.map +1 -0
- package/dist/{track-D59FfpL0.cjs → track-Cb3Q4QmS.cjs} +4 -2
- package/dist/track-Cb3Q4QmS.cjs.map +1 -0
- package/dist/validate.cjs +1 -1
- package/dist/validate.js +1 -1
- package/dist/webhook.cjs +1 -1
- package/dist/webhook.js +1 -1
- package/dist/workflow-distributed.cjs +1 -1
- package/dist/workflow-distributed.js +1 -1
- package/dist/workflow.cjs +3 -1
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts.map +1 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +3 -3
- package/dist/workflow.js.map +1 -1
- package/dist/yaml-config.cjs +233 -4
- package/dist/yaml-config.cjs.map +1 -0
- package/dist/yaml-config.d.cts.map +1 -1
- package/dist/yaml-config.d.ts.map +1 -1
- package/dist/yaml-config.js +8 -7
- package/dist/yaml-config.js.map +1 -1
- package/package.json +1 -2
- package/dist/functional-DtI0u4vx.js.map +0 -1
- package/dist/functional-zpzNLhky.cjs.map +0 -1
- package/dist/logger-thMPLpOG.cjs +0 -487
- package/dist/logger-thMPLpOG.cjs.map +0 -1
- package/dist/node-require-DF5QBX6z.cjs.map +0 -1
- package/dist/node-require-Db1oDpLj.js.map +0 -1
- package/dist/operation-context-C-2hmmtP.js.map +0 -1
- package/dist/operation-context-n4_obUwq.cjs.map +0 -1
- package/dist/stable-hash-BNTMrmdB.cjs.map +0 -1
- package/dist/stable-hash-Cg5cT34Q.js.map +0 -1
- package/dist/track-D59FfpL0.cjs.map +0 -1
- package/dist/track-wc0HafS_.js.map +0 -1
- package/dist/yaml-config-Ck2uB0Dp.cjs +0 -273
- package/dist/yaml-config-Ck2uB0Dp.cjs.map +0 -1
- package/src/attribute-redacting-processor.test.ts +0 -763
- package/src/attribute-redacting-processor.ts +0 -621
- package/src/attributes/attachers.ts +0 -161
- package/src/attributes/builders.ts +0 -529
- package/src/attributes/domains.ts +0 -42
- package/src/attributes/index.ts +0 -81
- package/src/attributes/registry.ts +0 -323
- package/src/attributes/types.ts +0 -211
- package/src/attributes/utils.ts +0 -64
- package/src/attributes/validators.ts +0 -266
- package/src/attributes.test.ts +0 -292
- package/src/auto.ts +0 -67
- package/src/autotel-logger.test.ts +0 -548
- package/src/autotel-logger.ts +0 -364
- package/src/baggage-span-processor.test.ts +0 -202
- package/src/baggage-span-processor.ts +0 -100
- package/src/business-baggage.test.ts +0 -500
- package/src/business-baggage.ts +0 -669
- package/src/circuit-breaker.test.ts +0 -341
- package/src/circuit-breaker.ts +0 -184
- package/src/config.test.ts +0 -94
- package/src/config.ts +0 -172
- package/src/correlated-events.test.ts +0 -151
- package/src/correlated-events.ts +0 -47
- package/src/correlation-id.test.ts +0 -163
- package/src/correlation-id.ts +0 -206
- package/src/db.test.ts +0 -252
- package/src/db.ts +0 -447
- package/src/decorators.test.ts +0 -153
- package/src/decorators.ts +0 -188
- package/src/define-event.test.ts +0 -41
- package/src/define-event.ts +0 -58
- package/src/devtools.ts +0 -60
- package/src/drain-pipeline.test.ts +0 -68
- package/src/drain-pipeline.ts +0 -199
- package/src/drain-toolkit.test.ts +0 -113
- package/src/drain-toolkit.ts +0 -129
- package/src/enricher-toolkit.test.ts +0 -67
- package/src/enricher-toolkit.ts +0 -79
- package/src/enrichers.test.ts +0 -150
- package/src/enrichers.ts +0 -145
- package/src/env-config.test.ts +0 -323
- package/src/env-config.ts +0 -309
- package/src/error-catalog.test.ts +0 -133
- package/src/error-catalog.ts +0 -262
- package/src/event-queue.test.ts +0 -864
- package/src/event-queue.ts +0 -699
- package/src/event-subscriber.ts +0 -262
- package/src/event-testing.ts +0 -197
- package/src/event.test.ts +0 -1104
- package/src/event.ts +0 -988
- package/src/events-config.ts +0 -235
- package/src/exporters.ts +0 -165
- package/src/filtering-span-processor.test.ts +0 -281
- package/src/filtering-span-processor.ts +0 -111
- package/src/flatten-attributes.test.ts +0 -76
- package/src/flatten-attributes.ts +0 -80
- package/src/functional.strict-types.typecheck.ts +0 -53
- package/src/functional.test.ts +0 -1464
- package/src/functional.ts +0 -2539
- package/src/functional.types.test.ts +0 -135
- package/src/hook.mjs +0 -15
- package/src/http.test.ts +0 -485
- package/src/http.ts +0 -424
- package/src/index.ts +0 -433
- package/src/init-auto-redactor.test.ts +0 -53
- package/src/init-redactor.test.ts +0 -8
- package/src/init.customization.test.ts +0 -665
- package/src/init.integrations.test.ts +0 -399
- package/src/init.openllmetry.test.ts +0 -194
- package/src/init.protocol.test.ts +0 -215
- package/src/init.ts +0 -2439
- package/src/instrumentation.test.ts +0 -108
- package/src/instrumentation.ts +0 -319
- package/src/logger.test.ts +0 -125
- package/src/logger.ts +0 -341
- package/src/messaging-adapters.test.ts +0 -595
- package/src/messaging-adapters.ts +0 -583
- package/src/messaging-testing.test.ts +0 -573
- package/src/messaging-testing.ts +0 -935
- package/src/messaging.test.ts +0 -1646
- package/src/messaging.ts +0 -2245
- package/src/metric-helpers.ts +0 -47
- package/src/metric-testing.ts +0 -197
- package/src/metric.ts +0 -446
- package/src/metrics.test.ts +0 -241
- package/src/node-require.ts +0 -123
- package/src/operation-context.ts +0 -93
- package/src/parse-error.test.ts +0 -73
- package/src/parse-error.ts +0 -112
- package/src/posthog-logs.test.ts +0 -115
- package/src/posthog-logs.ts +0 -77
- package/src/pretty-console-exporter.test.ts +0 -545
- package/src/pretty-console-exporter.ts +0 -413
- package/src/pretty-log-formatter.test.ts +0 -123
- package/src/pretty-log-formatter.ts +0 -210
- package/src/processors/canonical-log-line-processor.test.ts +0 -523
- package/src/processors/canonical-log-line-processor.ts +0 -396
- package/src/processors.ts +0 -152
- package/src/rate-limiter.test.ts +0 -199
- package/src/rate-limiter.ts +0 -98
- package/src/redact-values.test.ts +0 -90
- package/src/redact-values.ts +0 -34
- package/src/register.ts +0 -37
- package/src/request-logger.test.ts +0 -545
- package/src/request-logger.ts +0 -342
- package/src/sampling.test.ts +0 -1060
- package/src/sampling.ts +0 -737
- package/src/security-schema.test.ts +0 -45
- package/src/security-schema.ts +0 -107
- package/src/semantic-conventions.ts +0 -15
- package/src/semantic-helpers.test.ts +0 -226
- package/src/semantic-helpers.ts +0 -438
- package/src/shutdown.test.ts +0 -364
- package/src/shutdown.ts +0 -246
- package/src/span-name-normalizer.test.ts +0 -377
- package/src/span-name-normalizer.ts +0 -213
- package/src/stable-hash.ts +0 -27
- package/src/structured-error.test.ts +0 -191
- package/src/structured-error.ts +0 -157
- package/src/stub.integration.test.ts +0 -361
- package/src/tail-sampling-processor.test.ts +0 -230
- package/src/tail-sampling-processor.ts +0 -55
- package/src/test-span-collector.test.ts +0 -234
- package/src/test-span-collector.ts +0 -150
- package/src/testing.ts +0 -705
- package/src/trace-context.test.ts +0 -73
- package/src/trace-context.ts +0 -567
- package/src/trace-helpers.new.test.ts +0 -278
- package/src/trace-helpers.test.ts +0 -290
- package/src/trace-helpers.ts +0 -710
- package/src/trace-hybrid.test.ts +0 -42
- package/src/trace-hybrid.ts +0 -37
- package/src/tracer-provider.test.ts +0 -183
- package/src/tracer-provider.ts +0 -266
- package/src/track.test.ts +0 -154
- package/src/track.ts +0 -216
- package/src/validate.test.ts +0 -287
- package/src/validate.ts +0 -307
- package/src/validation-attributes.ts +0 -43
- package/src/validation.test.ts +0 -330
- package/src/validation.ts +0 -246
- package/src/variable-name-inference.test.ts +0 -178
- package/src/variable-name-inference.ts +0 -242
- package/src/webhook.test.ts +0 -649
- package/src/webhook.ts +0 -637
- package/src/workflow-distributed.test.ts +0 -786
- package/src/workflow-distributed.ts +0 -916
- package/src/workflow.async-safety.integration.test.ts +0 -345
- package/src/workflow.test.ts +0 -647
- package/src/workflow.ts +0 -810
- package/src/yaml-config.test.ts +0 -373
- package/src/yaml-config.ts +0 -351
package/src/validate.ts
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation telemetry — connect runtime input validation (Zod or any
|
|
3
|
-
* `safeParse` schema) to your traces and metrics at the boundaries where bad
|
|
4
|
-
* data actually enters: HTTP bodies, events, messages.
|
|
5
|
-
*
|
|
6
|
-
* Today a `safeParse` failure either throws (no span, no metric, no alert) or
|
|
7
|
-
* is silently swallowed in a handler. `defineValidator` makes the mismatch
|
|
8
|
-
* **observable** — a `validation.*` span attribute set and a counter
|
|
9
|
-
* incremented — with a per-validator `observe` vs `reject` mode:
|
|
10
|
-
*
|
|
11
|
-
* - `reject` (default): record telemetry, then throw a structured 400-shaped
|
|
12
|
-
* error so the boundary can fail cleanly.
|
|
13
|
-
* - `observe`: record telemetry, return the raw input so the handler continues
|
|
14
|
-
* — useful for measuring real-world drift before you enforce it.
|
|
15
|
-
*
|
|
16
|
-
* **Not a security feature by default.** A malformed body is usually a bug or
|
|
17
|
-
* version skew, not an attack. Validation telemetry is first-class on its own
|
|
18
|
-
* metric; escalation to the security path is a deliberate opt-in via
|
|
19
|
-
* {@link onValidationMismatch} (e.g. wired by `autotel-audit`), never automatic.
|
|
20
|
-
*
|
|
21
|
-
* **PII-safe by construction.** Only field *paths*, issue *codes*, and the
|
|
22
|
-
* declared *type* are ever recorded — never the offending value, and never a
|
|
23
|
-
* validator's error `message` (which routinely embeds the received value).
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { trace } from '@opentelemetry/api';
|
|
27
|
-
import { createCounter } from './metric-helpers';
|
|
28
|
-
import {
|
|
29
|
-
createStructuredError,
|
|
30
|
-
type StructuredError,
|
|
31
|
-
} from './structured-error';
|
|
32
|
-
import { hashJson } from './stable-hash';
|
|
33
|
-
import type { SchemaLike } from './define-event';
|
|
34
|
-
import {
|
|
35
|
-
VALIDATION_ATTR,
|
|
36
|
-
VALIDATION_ISSUE_CAP,
|
|
37
|
-
VALIDATION_METRICS,
|
|
38
|
-
} from './validation-attributes';
|
|
39
|
-
|
|
40
|
-
export type { SchemaLike } from './define-event';
|
|
41
|
-
|
|
42
|
-
export type ValidationMode = 'observe' | 'reject';
|
|
43
|
-
export type ValidationSeverity = 'info' | 'warning' | 'error';
|
|
44
|
-
|
|
45
|
-
/** A single failing field, stripped of any payload values. */
|
|
46
|
-
export interface ValidationIssue {
|
|
47
|
-
/** Dotted field path, e.g. `items.0.price`. Never a value. */
|
|
48
|
-
path: string;
|
|
49
|
-
/** Issue code (e.g. Zod's `invalid_type`, `too_small`). Never a value. */
|
|
50
|
-
code: string;
|
|
51
|
-
/** Declared type/constraint summary, e.g. `string`. Never a received value. */
|
|
52
|
-
expected?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Everything the recorder needs — already PII-stripped by the caller. */
|
|
56
|
-
export interface ValidationMismatch {
|
|
57
|
-
/** Contract id, e.g. `POST /orders` or `order.placed`. */
|
|
58
|
-
name: string;
|
|
59
|
-
boundary: string;
|
|
60
|
-
mode: ValidationMode;
|
|
61
|
-
issues: ValidationIssue[];
|
|
62
|
-
hash?: string;
|
|
63
|
-
severity?: ValidationSeverity;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
let mismatchCounter: ReturnType<typeof createCounter> | undefined;
|
|
67
|
-
function counter(): ReturnType<typeof createCounter> {
|
|
68
|
-
if (!mismatchCounter) {
|
|
69
|
-
mismatchCounter = createCounter(VALIDATION_METRICS.mismatches, {
|
|
70
|
-
description: 'Input payloads that did not match their declared shape',
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
return mismatchCounter;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
type MismatchListener = (mismatch: ValidationMismatch) => void;
|
|
77
|
-
const listeners = new Set<MismatchListener>();
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Register an explicit handler called on every recorded mismatch — the opt-in
|
|
81
|
-
* seam for escalating to security events, a webhook, or a custom sink. There is
|
|
82
|
-
* no automatic, package-presence-driven escalation: nothing fires here unless
|
|
83
|
-
* you (or a package you wire up) register a handler.
|
|
84
|
-
*
|
|
85
|
-
* Multiple subscribers coexist: a package (e.g. `autotel-audit` bridging to
|
|
86
|
-
* security events) and your own app code (a webhook, a logger) can both
|
|
87
|
-
* register and all fire. Returns an unsubscribe fn that removes only this
|
|
88
|
-
* handler; registering the same function twice is a no-op (Set semantics).
|
|
89
|
-
*/
|
|
90
|
-
export function onValidationMismatch(handler: MismatchListener): () => void {
|
|
91
|
-
listeners.add(handler);
|
|
92
|
-
return () => {
|
|
93
|
-
listeners.delete(handler);
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const truncate = (values: string[]): string =>
|
|
98
|
-
values.slice(0, VALIDATION_ISSUE_CAP).join(',');
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Record a validation mismatch as telemetry: `validation.*` attributes on the
|
|
102
|
-
* active span (if any) and an increment on `autotel.validation.mismatches`.
|
|
103
|
-
* Fail-open — never throws, so instrumentation can't break the boundary.
|
|
104
|
-
*/
|
|
105
|
-
export function recordValidationMismatch(mismatch: ValidationMismatch): void {
|
|
106
|
-
try {
|
|
107
|
-
const paths = mismatch.issues.map((i) => i.path).filter(Boolean);
|
|
108
|
-
const codes = [...new Set(mismatch.issues.map((i) => i.code))];
|
|
109
|
-
|
|
110
|
-
const span = trace.getActiveSpan();
|
|
111
|
-
if (span) {
|
|
112
|
-
span.setAttributes({
|
|
113
|
-
[VALIDATION_ATTR.name]: mismatch.name,
|
|
114
|
-
[VALIDATION_ATTR.boundary]: mismatch.boundary,
|
|
115
|
-
[VALIDATION_ATTR.mode]: mismatch.mode,
|
|
116
|
-
[VALIDATION_ATTR.issueCount]: mismatch.issues.length,
|
|
117
|
-
[VALIDATION_ATTR.issuePaths]: truncate(paths),
|
|
118
|
-
[VALIDATION_ATTR.issueCodes]: truncate(codes),
|
|
119
|
-
...(mismatch.hash ? { [VALIDATION_ATTR.hash]: mismatch.hash } : {}),
|
|
120
|
-
...(mismatch.severity
|
|
121
|
-
? { [VALIDATION_ATTR.severity]: mismatch.severity }
|
|
122
|
-
: {}),
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
counter().add(1, {
|
|
128
|
-
boundary: mismatch.boundary,
|
|
129
|
-
validation: mismatch.name,
|
|
130
|
-
mode: mismatch.mode,
|
|
131
|
-
});
|
|
132
|
-
} catch {
|
|
133
|
-
// meter not initialised yet — skip the count, keep the span attrs
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Dispatch to every subscriber with per-listener fault isolation: one
|
|
137
|
-
// throwing subscriber must not starve its peers or break the boundary.
|
|
138
|
-
// Set iteration tolerates concurrent (un)subscription safely.
|
|
139
|
-
for (const listener of listeners) {
|
|
140
|
-
try {
|
|
141
|
-
listener(mismatch);
|
|
142
|
-
} catch {
|
|
143
|
-
// a misbehaving subscriber must not break the boundary or its peers
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
} catch {
|
|
147
|
-
// fail-open: telemetry must never break the validated boundary
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Normalise an arbitrary validation error into PII-safe issues. Reads only
|
|
153
|
-
* `path`, `code`, and (when it is a declared type name) `expected` — and never
|
|
154
|
-
* `message`, `received`, or any value-bearing field. Understands the Zod shape
|
|
155
|
-
* (`error.issues`) and a generic `error.errors` fallback; returns `[]` for
|
|
156
|
-
* anything unrecognised.
|
|
157
|
-
*/
|
|
158
|
-
export function formatValidationIssues(error: unknown): ValidationIssue[] {
|
|
159
|
-
const raw = extractRawIssues(error);
|
|
160
|
-
return raw.map((issue) => toSafeIssue(issue));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function extractRawIssues(error: unknown): Array<Record<string, unknown>> {
|
|
164
|
-
if (error && typeof error === 'object') {
|
|
165
|
-
const candidate =
|
|
166
|
-
(error as { issues?: unknown }).issues ??
|
|
167
|
-
(error as { errors?: unknown }).errors;
|
|
168
|
-
if (Array.isArray(candidate)) {
|
|
169
|
-
return candidate.filter(
|
|
170
|
-
(i): i is Record<string, unknown> =>
|
|
171
|
-
i !== null && typeof i === 'object',
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return [];
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function toSafeIssue(issue: Record<string, unknown>): ValidationIssue {
|
|
179
|
-
const rawPath = issue.path;
|
|
180
|
-
const path = Array.isArray(rawPath)
|
|
181
|
-
? rawPath.map(String).join('.')
|
|
182
|
-
: typeof rawPath === 'string'
|
|
183
|
-
? rawPath
|
|
184
|
-
: '';
|
|
185
|
-
const code = typeof issue.code === 'string' ? issue.code : 'invalid';
|
|
186
|
-
// `expected` is a declared type name in Zod (e.g. 'string'); safe. We never
|
|
187
|
-
// read `received`/`message`/`value`, which can carry the offending payload.
|
|
188
|
-
const expected =
|
|
189
|
-
typeof issue.expected === 'string' ? issue.expected : undefined;
|
|
190
|
-
return expected ? { path, code, expected } : { path, code };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export interface DefineValidatorOptions<S> {
|
|
194
|
-
/** Where validation runs. Defaults to `input`. */
|
|
195
|
-
boundary?: string;
|
|
196
|
-
/** `reject` (default): record then throw. `observe`: record then continue. */
|
|
197
|
-
onMismatch?: ValidationMode;
|
|
198
|
-
/** Project the schema to JSON Schema for a stable `validation.hash`. */
|
|
199
|
-
toJsonSchema?: (schema: S) => unknown;
|
|
200
|
-
severity?: ValidationSeverity;
|
|
201
|
-
/** Build the error thrown in `reject` mode (defaults to a 400 structured error). */
|
|
202
|
-
onReject?: (issues: ValidationIssue[], name: string) => Error;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export type ValidatorResult<T> =
|
|
206
|
-
| { success: true; data: T }
|
|
207
|
-
| { success: false; issues: ValidationIssue[] };
|
|
208
|
-
|
|
209
|
-
export interface Validator<T> {
|
|
210
|
-
readonly name: string;
|
|
211
|
-
readonly mode: ValidationMode;
|
|
212
|
-
/** Validate and record on failure; never throws. */
|
|
213
|
-
safeParse(input: unknown): ValidatorResult<T>;
|
|
214
|
-
/**
|
|
215
|
-
* Validate, record on failure, then apply the mode: `reject` throws,
|
|
216
|
-
* `observe` returns the raw input so the handler can continue.
|
|
217
|
-
*/
|
|
218
|
-
parse(input: unknown): T;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function defaultRejectError(
|
|
222
|
-
issues: ValidationIssue[],
|
|
223
|
-
name: string,
|
|
224
|
-
): StructuredError {
|
|
225
|
-
return createStructuredError({
|
|
226
|
-
name: 'ValidationError',
|
|
227
|
-
status: 400,
|
|
228
|
-
code: 'validation_failed',
|
|
229
|
-
message: `Input for "${name}" did not match its declared shape.`,
|
|
230
|
-
why: `${issues.length} field(s) failed validation: ${issues
|
|
231
|
-
.map((i) => i.path || '(root)')
|
|
232
|
-
.slice(0, VALIDATION_ISSUE_CAP)
|
|
233
|
-
.join(', ')}.`,
|
|
234
|
-
fix: 'Send a payload that matches the schema, or switch this validator to observe mode while you investigate.',
|
|
235
|
-
// PII-safe: paths + codes only, no received values.
|
|
236
|
-
details: { validation: name, issues },
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Declare an expected input shape once and get a validator that records every
|
|
242
|
-
* mismatch as telemetry.
|
|
243
|
-
*
|
|
244
|
-
* @example
|
|
245
|
-
* ```ts
|
|
246
|
-
* import { z } from 'zod';
|
|
247
|
-
* import { defineValidator } from 'autotel/validate';
|
|
248
|
-
*
|
|
249
|
-
* const OrderBody = defineValidator('POST /orders', z.object({
|
|
250
|
-
* items: z.array(z.object({ sku: z.string(), qty: z.number().int() })),
|
|
251
|
-
* }), { boundary: 'http', toJsonSchema: (s) => z.toJSONSchema(s) });
|
|
252
|
-
*
|
|
253
|
-
* // reject mode (default): records + throws a 400-shaped structured error
|
|
254
|
-
* const order = OrderBody.parse(req.body);
|
|
255
|
-
*
|
|
256
|
-
* // observe mode: records, returns the result, never throws
|
|
257
|
-
* const result = OrderBody.safeParse(req.body);
|
|
258
|
-
* if (!result.success) metrics.onDrift(result.issues);
|
|
259
|
-
* ```
|
|
260
|
-
*/
|
|
261
|
-
export function defineValidator<T, S extends SchemaLike<T>>(
|
|
262
|
-
name: string,
|
|
263
|
-
schema: S,
|
|
264
|
-
options: DefineValidatorOptions<S> = {},
|
|
265
|
-
): Validator<T> {
|
|
266
|
-
const mode = options.onMismatch ?? 'reject';
|
|
267
|
-
const boundary = options.boundary ?? 'input';
|
|
268
|
-
const hash = options.toJsonSchema
|
|
269
|
-
? hashJson(options.toJsonSchema(schema))
|
|
270
|
-
: undefined;
|
|
271
|
-
|
|
272
|
-
const record = (issues: ValidationIssue[]): void => {
|
|
273
|
-
recordValidationMismatch({
|
|
274
|
-
name,
|
|
275
|
-
boundary,
|
|
276
|
-
mode,
|
|
277
|
-
issues,
|
|
278
|
-
hash,
|
|
279
|
-
severity: options.severity,
|
|
280
|
-
});
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
name,
|
|
285
|
-
mode,
|
|
286
|
-
safeParse(input: unknown): ValidatorResult<T> {
|
|
287
|
-
const parsed = schema.safeParse(input);
|
|
288
|
-
if (parsed.success) return { success: true, data: parsed.data };
|
|
289
|
-
const issues = formatValidationIssues(parsed.error);
|
|
290
|
-
record(issues);
|
|
291
|
-
return { success: false, issues };
|
|
292
|
-
},
|
|
293
|
-
parse(input: unknown): T {
|
|
294
|
-
const parsed = schema.safeParse(input);
|
|
295
|
-
if (parsed.success) return parsed.data;
|
|
296
|
-
const issues = formatValidationIssues(parsed.error);
|
|
297
|
-
record(issues);
|
|
298
|
-
if (mode === 'reject') {
|
|
299
|
-
throw (
|
|
300
|
-
options.onReject?.(issues, name) ?? defaultRejectError(issues, name)
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
// observe: continue with the raw input (documented type caveat)
|
|
304
|
-
return input as T;
|
|
305
|
-
},
|
|
306
|
-
};
|
|
307
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation telemetry wire constants — the single source of truth for the
|
|
3
|
-
* `validation.*` span attributes and the `autotel.validation.mismatches` metric
|
|
4
|
-
* emitted when an input payload (HTTP body, event, message) fails to match its
|
|
5
|
-
* declared shape.
|
|
6
|
-
*
|
|
7
|
-
* Dependency-free and side-effect-free by design (mirrors `security-schema.ts`):
|
|
8
|
-
* safe to import from anything that only needs the constant strings — a
|
|
9
|
-
* dashboard, a CLI, an alert rule — without pulling in the OpenTelemetry SDK.
|
|
10
|
-
*
|
|
11
|
-
* These keys are a public API for the agents that query your telemetry. Treat a
|
|
12
|
-
* rename here the way you'd treat a breaking change to any other contract.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export const VALIDATION_ATTR = {
|
|
16
|
-
/** Contract id of the validated boundary, e.g. `POST /orders`, `order.placed`. */
|
|
17
|
-
name: 'validation.name',
|
|
18
|
-
/** Where validation ran: `http` | `event` | `message` | a custom label. */
|
|
19
|
-
boundary: 'validation.boundary',
|
|
20
|
-
/** `observe` (recorded, request continues) or `reject` (recorded, then failed). */
|
|
21
|
-
mode: 'validation.mode',
|
|
22
|
-
/** Stable hash of the declared shape, when a JSON-schema projection is given. */
|
|
23
|
-
hash: 'validation.hash',
|
|
24
|
-
/** `info` | `warning` | `error`. */
|
|
25
|
-
severity: 'validation.severity',
|
|
26
|
-
/** Number of failing fields. */
|
|
27
|
-
issueCount: 'validation.issue.count',
|
|
28
|
-
/** Comma-separated failing field paths (capped). Never contains values. */
|
|
29
|
-
issuePaths: 'validation.issue.paths',
|
|
30
|
-
/** Comma-separated distinct issue codes (capped). Never contains values. */
|
|
31
|
-
issueCodes: 'validation.issue.codes',
|
|
32
|
-
} as const;
|
|
33
|
-
|
|
34
|
-
export type ValidationAttributeKey =
|
|
35
|
-
(typeof VALIDATION_ATTR)[keyof typeof VALIDATION_ATTR];
|
|
36
|
-
|
|
37
|
-
export const VALIDATION_METRICS = {
|
|
38
|
-
/** Counter, labelled `{ boundary, validation, mode }`. */
|
|
39
|
-
mismatches: 'autotel.validation.mismatches',
|
|
40
|
-
} as const;
|
|
41
|
-
|
|
42
|
-
/** Max field paths / codes stamped onto a span, to bound attribute size. */
|
|
43
|
-
export const VALIDATION_ISSUE_CAP = 20;
|
package/src/validation.test.ts
DELETED
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for input validation
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import {
|
|
7
|
-
validateEventName,
|
|
8
|
-
validateAttributes,
|
|
9
|
-
validateEvent,
|
|
10
|
-
ValidationError,
|
|
11
|
-
getDefaultValidationConfig,
|
|
12
|
-
} from './validation';
|
|
13
|
-
|
|
14
|
-
describe('validateEventName()', () => {
|
|
15
|
-
it('should accept valid event names', () => {
|
|
16
|
-
expect(validateEventName('user.signup')).toBe('user.signup');
|
|
17
|
-
expect(validateEventName('order_completed')).toBe('order_completed');
|
|
18
|
-
expect(validateEventName('feature-used')).toBe('feature-used');
|
|
19
|
-
expect(validateEventName('app123.event456')).toBe('app123.event456');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should trim whitespace', () => {
|
|
23
|
-
expect(validateEventName(' user.signup ')).toBe('user.signup');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should reject empty event names', () => {
|
|
27
|
-
expect(() => validateEventName('')).toThrow(ValidationError);
|
|
28
|
-
expect(() => validateEventName(' ')).toThrow(ValidationError);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should reject non-string event names', () => {
|
|
32
|
-
expect(() => validateEventName(123 as any)).toThrow(ValidationError);
|
|
33
|
-
|
|
34
|
-
expect(() => validateEventName(null as any)).toThrow(ValidationError);
|
|
35
|
-
|
|
36
|
-
expect(() => validateEventName(undefined as any)).toThrow(ValidationError);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should reject event names that are too long', () => {
|
|
40
|
-
const longName = 'a'.repeat(101);
|
|
41
|
-
expect(() => validateEventName(longName)).toThrow(ValidationError);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should reject event names with invalid characters', () => {
|
|
45
|
-
expect(() => validateEventName('user signup')).toThrow(ValidationError);
|
|
46
|
-
expect(() => validateEventName('user@signup')).toThrow(ValidationError);
|
|
47
|
-
expect(() => validateEventName('user/signup')).toThrow(ValidationError);
|
|
48
|
-
expect(() => validateEventName(String.raw`user\signup`)).toThrow(
|
|
49
|
-
ValidationError,
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('validateAttributes()', () => {
|
|
55
|
-
it('should accept valid attributes', () => {
|
|
56
|
-
const attrs = {
|
|
57
|
-
userId: '123',
|
|
58
|
-
plan: 'pro',
|
|
59
|
-
count: 5,
|
|
60
|
-
active: true,
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
expect(validateAttributes(attrs)).toEqual(attrs);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should handle undefined attributes', () => {
|
|
67
|
-
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
68
|
-
expect(validateAttributes(undefined)).toBeUndefined();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('should reject non-object attributes', () => {
|
|
72
|
-
expect(() => validateAttributes('string' as any)).toThrow(ValidationError);
|
|
73
|
-
|
|
74
|
-
expect(() => validateAttributes(123 as any)).toThrow(ValidationError);
|
|
75
|
-
|
|
76
|
-
expect(() => validateAttributes([] as any)).toThrow(ValidationError);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should reject too many attributes', () => {
|
|
80
|
-
const config = getDefaultValidationConfig();
|
|
81
|
-
const attrs: Record<string, unknown> = {};
|
|
82
|
-
for (let i = 0; i < config.maxAttributeCount + 1; i++) {
|
|
83
|
-
attrs[`key${i}`] = 'value';
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
expect(() =>
|
|
87
|
-
validateAttributes(attrs as Record<string, string | number | boolean>),
|
|
88
|
-
).toThrow(ValidationError);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should reject attribute keys that are too long', () => {
|
|
92
|
-
const longKey = 'a'.repeat(101);
|
|
93
|
-
const attrs = { [longKey]: 'value' };
|
|
94
|
-
|
|
95
|
-
expect(() => validateAttributes(attrs)).toThrow(ValidationError);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should truncate long string values', () => {
|
|
99
|
-
const longValue = 'a'.repeat(1500);
|
|
100
|
-
const attrs = { field: longValue };
|
|
101
|
-
|
|
102
|
-
const result = validateAttributes(attrs);
|
|
103
|
-
expect(result?.field).toBe('a'.repeat(1000) + '...');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should redact sensitive fields', () => {
|
|
107
|
-
const attrs = {
|
|
108
|
-
email: 'user@example.com',
|
|
109
|
-
password: 'secret123',
|
|
110
|
-
apiKey: 'abc123',
|
|
111
|
-
normalField: 'value',
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const result = validateAttributes(attrs);
|
|
115
|
-
expect(result?.email).toBe('user@example.com');
|
|
116
|
-
expect(result?.password).toBe('[REDACTED]');
|
|
117
|
-
expect(result?.apiKey).toBe('[REDACTED]');
|
|
118
|
-
expect(result?.normalField).toBe('value');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should redact sensitive fields in nested objects', () => {
|
|
122
|
-
const attrs = {
|
|
123
|
-
user: {
|
|
124
|
-
password: 'secret123',
|
|
125
|
-
apiKey: 'abc123',
|
|
126
|
-
},
|
|
127
|
-
session: {
|
|
128
|
-
authToken: 'token-123',
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const result = validateAttributes(attrs) as
|
|
133
|
-
| Record<string, Record<string, unknown>>
|
|
134
|
-
| undefined;
|
|
135
|
-
|
|
136
|
-
expect(result?.user?.password).toBe('[REDACTED]');
|
|
137
|
-
expect(result?.user?.apiKey).toBe('[REDACTED]');
|
|
138
|
-
expect(result?.session?.authToken).toBe('[REDACTED]');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('should handle nested objects within depth limit', () => {
|
|
142
|
-
const attrs = {
|
|
143
|
-
user: {
|
|
144
|
-
profile: {
|
|
145
|
-
name: 'John',
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const result = validateAttributes(
|
|
151
|
-
attrs as unknown as Record<string, string | number | boolean>,
|
|
152
|
-
);
|
|
153
|
-
expect(result).toEqual(attrs);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('should truncate deeply nested objects', () => {
|
|
157
|
-
const attrs = {
|
|
158
|
-
level1: {
|
|
159
|
-
level2: {
|
|
160
|
-
level3: {
|
|
161
|
-
level4: {
|
|
162
|
-
tooDeep: 'value',
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const result = validateAttributes(attrs as any) as any;
|
|
170
|
-
expect(result.level1.level2.level3.level4).toBe('[MAX_DEPTH_EXCEEDED]');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('should handle arrays', () => {
|
|
174
|
-
const attrs = {
|
|
175
|
-
tags: ['tag1', 'tag2', 'tag3'],
|
|
176
|
-
scores: [1, 2, 3],
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const result = validateAttributes(attrs as any);
|
|
180
|
-
expect(result).toEqual(attrs);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('should handle circular references', () => {
|
|
184
|
-
const circular: any = { name: 'test' };
|
|
185
|
-
circular.self = circular;
|
|
186
|
-
|
|
187
|
-
const attrs = { data: circular };
|
|
188
|
-
|
|
189
|
-
const result = validateAttributes(attrs) as any;
|
|
190
|
-
expect(result.data).toBe('[CIRCULAR]');
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('should handle null and undefined values', () => {
|
|
194
|
-
const attrs = {
|
|
195
|
-
nullable: null,
|
|
196
|
-
undefinedField: undefined,
|
|
197
|
-
normalField: 'value',
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const result = validateAttributes(attrs as any);
|
|
201
|
-
expect(result?.nullable).toBeNull();
|
|
202
|
-
expect(result?.undefinedField).toBeUndefined();
|
|
203
|
-
expect(result?.normalField).toBe('value');
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should handle unsupported types', () => {
|
|
207
|
-
const attrs = {
|
|
208
|
-
func: () => {},
|
|
209
|
-
symbol: Symbol('test'),
|
|
210
|
-
normalField: 'value',
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const result = validateAttributes(attrs as any);
|
|
214
|
-
expect(result?.func).toBe('[function]');
|
|
215
|
-
expect(result?.symbol).toBe('[symbol]');
|
|
216
|
-
expect(result?.normalField).toBe('value');
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
describe('validateEvent()', () => {
|
|
221
|
-
it('should validate both event name and attributes', () => {
|
|
222
|
-
const result = validateEvent('user.signup', {
|
|
223
|
-
userId: '123',
|
|
224
|
-
password: 'secret',
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
expect(result.eventName).toBe('user.signup');
|
|
228
|
-
expect(result.attributes?.userId).toBe('123');
|
|
229
|
-
expect(result.attributes?.password).toBe('[REDACTED]');
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('should handle events without attributes', () => {
|
|
233
|
-
const result = validateEvent('page.viewed');
|
|
234
|
-
|
|
235
|
-
expect(result.eventName).toBe('page.viewed');
|
|
236
|
-
expect(result.attributes).toBeUndefined();
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('should allow custom validation config', () => {
|
|
240
|
-
const result = validateEvent(
|
|
241
|
-
'test.event',
|
|
242
|
-
{ field: 'value' },
|
|
243
|
-
{ maxEventNameLength: 50 },
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
expect(result.eventName).toBe('test.event');
|
|
247
|
-
expect(result.attributes?.field).toBe('value');
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it('should throw on invalid event name', () => {
|
|
251
|
-
expect(() => validateEvent('', { userId: '123' })).toThrow(ValidationError);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('should throw on invalid attributes', () => {
|
|
255
|
-
expect(() => validateEvent('user.signup', 'invalid' as any)).toThrow(
|
|
256
|
-
ValidationError,
|
|
257
|
-
);
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
describe('Sensitive data patterns', () => {
|
|
262
|
-
it('should redact password fields', () => {
|
|
263
|
-
const attrs = {
|
|
264
|
-
password: 'secret',
|
|
265
|
-
userPassword: 'secret',
|
|
266
|
-
PASSWORD: 'secret',
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
const result = validateAttributes(attrs);
|
|
270
|
-
expect(result?.password).toBe('[REDACTED]');
|
|
271
|
-
expect(result?.userPassword).toBe('[REDACTED]');
|
|
272
|
-
expect(result?.PASSWORD).toBe('[REDACTED]');
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it('should redact token fields', () => {
|
|
276
|
-
const attrs = {
|
|
277
|
-
token: 'abc123',
|
|
278
|
-
accessToken: 'abc123',
|
|
279
|
-
auth_token: 'abc123',
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
const result = validateAttributes(attrs);
|
|
283
|
-
expect(result?.token).toBe('[REDACTED]');
|
|
284
|
-
expect(result?.accessToken).toBe('[REDACTED]');
|
|
285
|
-
expect(result?.auth_token).toBe('[REDACTED]');
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('should redact API key fields', () => {
|
|
289
|
-
const attrs = {
|
|
290
|
-
apiKey: 'abc123',
|
|
291
|
-
api_key: 'abc123',
|
|
292
|
-
API_KEY: 'abc123',
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
const result = validateAttributes(attrs);
|
|
296
|
-
expect(result?.apiKey).toBe('[REDACTED]');
|
|
297
|
-
expect(result?.api_key).toBe('[REDACTED]');
|
|
298
|
-
expect(result?.API_KEY).toBe('[REDACTED]');
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('should redact auth fields (strings only)', () => {
|
|
302
|
-
const attrs = {
|
|
303
|
-
auth: 'abc123',
|
|
304
|
-
authorization: 'Bearer token',
|
|
305
|
-
// `authenticated` matches the /auth/i key pattern, but `true` is a
|
|
306
|
-
// boolean status — not a credential — so it passes through unchanged.
|
|
307
|
-
// Redacting it to the string '[REDACTED]' would silently corrupt its
|
|
308
|
-
// type without protecting any secret.
|
|
309
|
-
authenticated: true,
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
const result = validateAttributes(attrs);
|
|
313
|
-
expect(result?.auth).toBe('[REDACTED]');
|
|
314
|
-
expect(result?.authorization).toBe('[REDACTED]');
|
|
315
|
-
expect(result?.authenticated).toBe(true);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it('should not redact non-sensitive fields with similar names', () => {
|
|
319
|
-
const attrs = {
|
|
320
|
-
email: 'user@example.com',
|
|
321
|
-
username: 'john',
|
|
322
|
-
userId: '123',
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
const result = validateAttributes(attrs);
|
|
326
|
-
expect(result?.email).toBe('user@example.com');
|
|
327
|
-
expect(result?.username).toBe('john');
|
|
328
|
-
expect(result?.userId).toBe('123');
|
|
329
|
-
});
|
|
330
|
-
});
|