autotel 4.0.0 → 4.2.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 (232) hide show
  1. package/README.md +26 -1
  2. package/dist/auto.cjs +2 -2
  3. package/dist/auto.js +1 -1
  4. package/dist/correlation-id.cjs +1 -1
  5. package/dist/correlation-id.js +1 -1
  6. package/dist/decorators.cjs +1 -1
  7. package/dist/decorators.js +1 -1
  8. package/dist/{event-Dlqr4ZNL.cjs → event-BhHREDJk.cjs} +3 -3
  9. package/dist/{event-Dlqr4ZNL.cjs.map → event-BhHREDJk.cjs.map} +1 -1
  10. package/dist/{event-_58ryBjh.js → event-ByBTV9M2.js} +3 -3
  11. package/dist/{event-_58ryBjh.js.map → event-ByBTV9M2.js.map} +1 -1
  12. package/dist/event.cjs +1 -1
  13. package/dist/event.js +1 -1
  14. package/dist/{functional-BGkT8J-h.js → functional-DtI0u4vx.js} +19 -19
  15. package/dist/functional-DtI0u4vx.js.map +1 -0
  16. package/dist/{functional-C4CzoVrX.cjs → functional-zpzNLhky.cjs} +4 -4
  17. package/dist/{functional-C4CzoVrX.cjs.map → functional-zpzNLhky.cjs.map} +1 -1
  18. package/dist/functional.cjs +1 -1
  19. package/dist/functional.js +1 -1
  20. package/dist/http.cjs +1 -1
  21. package/dist/http.js +1 -1
  22. package/dist/index.cjs +5 -5
  23. package/dist/index.d.cts +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +5 -5
  26. package/dist/{init-DJQOdVlN.d.ts → init-B7u-DjxM.d.ts} +57 -2
  27. package/dist/init-B7u-DjxM.d.ts.map +1 -0
  28. package/dist/{init-DvapOXCc.cjs → init-BX7AmFRl.cjs} +40 -21
  29. package/dist/init-BX7AmFRl.cjs.map +1 -0
  30. package/dist/{init-Ch6t7MNI.js → init-D-jnNMix.js} +39 -20
  31. package/dist/init-D-jnNMix.js.map +1 -0
  32. package/dist/{init-CNp-ee80.d.cts → init-DSrRmVnz.d.cts} +57 -2
  33. package/dist/init-DSrRmVnz.d.cts.map +1 -0
  34. package/dist/instrumentation.cjs +1 -1
  35. package/dist/instrumentation.js +1 -1
  36. package/dist/logger-D3Ej3DII.js +446 -0
  37. package/dist/logger-D3Ej3DII.js.map +1 -0
  38. package/dist/logger-thMPLpOG.cjs +487 -0
  39. package/dist/logger-thMPLpOG.cjs.map +1 -0
  40. package/dist/logger.cjs +8 -236
  41. package/dist/logger.js +2 -204
  42. package/dist/messaging.cjs +1 -1
  43. package/dist/messaging.js +1 -1
  44. package/dist/semantic-helpers.cjs +1 -1
  45. package/dist/semantic-helpers.js +1 -1
  46. package/dist/{track-3HY4NGV-.cjs → track-D59FfpL0.cjs} +2 -2
  47. package/dist/{track-3HY4NGV-.cjs.map → track-D59FfpL0.cjs.map} +1 -1
  48. package/dist/{track-nsKVy-pj.js → track-wc0HafS_.js} +6 -6
  49. package/dist/track-wc0HafS_.js.map +1 -0
  50. package/dist/webhook.cjs +1 -1
  51. package/dist/webhook.js +1 -1
  52. package/dist/workflow-distributed.cjs +1 -1
  53. package/dist/workflow-distributed.js +1 -1
  54. package/dist/workflow.cjs +1 -1
  55. package/dist/workflow.js +1 -1
  56. package/dist/{yaml-config-B3dQ82GR.cjs → yaml-config-Ck2uB0Dp.cjs} +2 -1
  57. package/dist/yaml-config-Ck2uB0Dp.cjs.map +1 -0
  58. package/dist/yaml-config.cjs +1 -1
  59. package/dist/yaml-config.d.cts +7 -1
  60. package/dist/yaml-config.d.cts.map +1 -1
  61. package/dist/yaml-config.d.ts +7 -1
  62. package/dist/yaml-config.d.ts.map +1 -1
  63. package/dist/yaml-config.js +1 -0
  64. package/dist/yaml-config.js.map +1 -1
  65. package/package.json +1 -2
  66. package/skills/autotel-core/SKILL.md +2 -0
  67. package/skills/autotel-instrumentation/SKILL.md +25 -0
  68. package/skills/debug-missing-spans/SKILL.md +3 -1
  69. package/skills/migrate-to-autotel/SKILL.md +24 -23
  70. package/skills/review-otel-patterns/SKILL.md +5 -4
  71. package/dist/functional-BGkT8J-h.js.map +0 -1
  72. package/dist/init-CNp-ee80.d.cts.map +0 -1
  73. package/dist/init-Ch6t7MNI.js.map +0 -1
  74. package/dist/init-DJQOdVlN.d.ts.map +0 -1
  75. package/dist/init-DvapOXCc.cjs.map +0 -1
  76. package/dist/logger.cjs.map +0 -1
  77. package/dist/logger.js.map +0 -1
  78. package/dist/track-nsKVy-pj.js.map +0 -1
  79. package/dist/yaml-config-B3dQ82GR.cjs.map +0 -1
  80. package/src/attribute-redacting-processor.test.ts +0 -763
  81. package/src/attribute-redacting-processor.ts +0 -621
  82. package/src/attributes/attachers.ts +0 -161
  83. package/src/attributes/builders.ts +0 -529
  84. package/src/attributes/domains.ts +0 -42
  85. package/src/attributes/index.ts +0 -81
  86. package/src/attributes/registry.ts +0 -323
  87. package/src/attributes/types.ts +0 -211
  88. package/src/attributes/utils.ts +0 -64
  89. package/src/attributes/validators.ts +0 -266
  90. package/src/attributes.test.ts +0 -292
  91. package/src/auto.ts +0 -67
  92. package/src/autotel-logger.test.ts +0 -548
  93. package/src/autotel-logger.ts +0 -364
  94. package/src/baggage-span-processor.test.ts +0 -202
  95. package/src/baggage-span-processor.ts +0 -100
  96. package/src/business-baggage.test.ts +0 -500
  97. package/src/business-baggage.ts +0 -669
  98. package/src/circuit-breaker.test.ts +0 -341
  99. package/src/circuit-breaker.ts +0 -184
  100. package/src/config.test.ts +0 -94
  101. package/src/config.ts +0 -172
  102. package/src/correlated-events.test.ts +0 -151
  103. package/src/correlated-events.ts +0 -47
  104. package/src/correlation-id.test.ts +0 -163
  105. package/src/correlation-id.ts +0 -206
  106. package/src/db.test.ts +0 -252
  107. package/src/db.ts +0 -447
  108. package/src/decorators.test.ts +0 -153
  109. package/src/decorators.ts +0 -188
  110. package/src/define-event.test.ts +0 -41
  111. package/src/define-event.ts +0 -58
  112. package/src/devtools.ts +0 -60
  113. package/src/drain-pipeline.test.ts +0 -68
  114. package/src/drain-pipeline.ts +0 -199
  115. package/src/drain-toolkit.test.ts +0 -113
  116. package/src/drain-toolkit.ts +0 -129
  117. package/src/enricher-toolkit.test.ts +0 -67
  118. package/src/enricher-toolkit.ts +0 -79
  119. package/src/enrichers.test.ts +0 -150
  120. package/src/enrichers.ts +0 -145
  121. package/src/env-config.test.ts +0 -323
  122. package/src/env-config.ts +0 -309
  123. package/src/error-catalog.test.ts +0 -133
  124. package/src/error-catalog.ts +0 -262
  125. package/src/event-queue.test.ts +0 -864
  126. package/src/event-queue.ts +0 -699
  127. package/src/event-subscriber.ts +0 -262
  128. package/src/event-testing.ts +0 -197
  129. package/src/event.test.ts +0 -1104
  130. package/src/event.ts +0 -988
  131. package/src/events-config.ts +0 -235
  132. package/src/exporters.ts +0 -165
  133. package/src/filtering-span-processor.test.ts +0 -281
  134. package/src/filtering-span-processor.ts +0 -111
  135. package/src/flatten-attributes.test.ts +0 -76
  136. package/src/flatten-attributes.ts +0 -80
  137. package/src/functional.strict-types.typecheck.ts +0 -53
  138. package/src/functional.test.ts +0 -1464
  139. package/src/functional.ts +0 -2539
  140. package/src/functional.types.test.ts +0 -135
  141. package/src/hook.mjs +0 -15
  142. package/src/http.test.ts +0 -485
  143. package/src/http.ts +0 -424
  144. package/src/index.ts +0 -433
  145. package/src/init-auto-redactor.test.ts +0 -53
  146. package/src/init-redactor.test.ts +0 -8
  147. package/src/init.customization.test.ts +0 -594
  148. package/src/init.integrations.test.ts +0 -399
  149. package/src/init.openllmetry.test.ts +0 -194
  150. package/src/init.protocol.test.ts +0 -215
  151. package/src/init.ts +0 -2312
  152. package/src/instrumentation.test.ts +0 -108
  153. package/src/instrumentation.ts +0 -319
  154. package/src/logger.test.ts +0 -125
  155. package/src/logger.ts +0 -341
  156. package/src/messaging-adapters.test.ts +0 -595
  157. package/src/messaging-adapters.ts +0 -583
  158. package/src/messaging-testing.test.ts +0 -573
  159. package/src/messaging-testing.ts +0 -935
  160. package/src/messaging.test.ts +0 -1646
  161. package/src/messaging.ts +0 -2245
  162. package/src/metric-helpers.ts +0 -47
  163. package/src/metric-testing.ts +0 -197
  164. package/src/metric.ts +0 -446
  165. package/src/metrics.test.ts +0 -241
  166. package/src/node-require.ts +0 -123
  167. package/src/operation-context.ts +0 -93
  168. package/src/parse-error.test.ts +0 -73
  169. package/src/parse-error.ts +0 -112
  170. package/src/posthog-logs.test.ts +0 -115
  171. package/src/posthog-logs.ts +0 -77
  172. package/src/pretty-console-exporter.test.ts +0 -545
  173. package/src/pretty-console-exporter.ts +0 -413
  174. package/src/pretty-log-formatter.test.ts +0 -123
  175. package/src/pretty-log-formatter.ts +0 -210
  176. package/src/processors/canonical-log-line-processor.test.ts +0 -523
  177. package/src/processors/canonical-log-line-processor.ts +0 -396
  178. package/src/processors.ts +0 -152
  179. package/src/rate-limiter.test.ts +0 -199
  180. package/src/rate-limiter.ts +0 -98
  181. package/src/redact-values.test.ts +0 -90
  182. package/src/redact-values.ts +0 -34
  183. package/src/register.ts +0 -37
  184. package/src/request-logger.test.ts +0 -545
  185. package/src/request-logger.ts +0 -342
  186. package/src/sampling.test.ts +0 -1060
  187. package/src/sampling.ts +0 -737
  188. package/src/security-schema.test.ts +0 -45
  189. package/src/security-schema.ts +0 -107
  190. package/src/semantic-conventions.ts +0 -15
  191. package/src/semantic-helpers.test.ts +0 -226
  192. package/src/semantic-helpers.ts +0 -438
  193. package/src/shutdown.test.ts +0 -364
  194. package/src/shutdown.ts +0 -246
  195. package/src/span-name-normalizer.test.ts +0 -377
  196. package/src/span-name-normalizer.ts +0 -213
  197. package/src/stable-hash.ts +0 -27
  198. package/src/structured-error.test.ts +0 -191
  199. package/src/structured-error.ts +0 -157
  200. package/src/stub.integration.test.ts +0 -361
  201. package/src/tail-sampling-processor.test.ts +0 -230
  202. package/src/tail-sampling-processor.ts +0 -55
  203. package/src/test-span-collector.test.ts +0 -234
  204. package/src/test-span-collector.ts +0 -150
  205. package/src/testing.ts +0 -705
  206. package/src/trace-context.test.ts +0 -73
  207. package/src/trace-context.ts +0 -567
  208. package/src/trace-helpers.new.test.ts +0 -278
  209. package/src/trace-helpers.test.ts +0 -290
  210. package/src/trace-helpers.ts +0 -710
  211. package/src/trace-hybrid.test.ts +0 -42
  212. package/src/trace-hybrid.ts +0 -37
  213. package/src/tracer-provider.test.ts +0 -183
  214. package/src/tracer-provider.ts +0 -266
  215. package/src/track.test.ts +0 -154
  216. package/src/track.ts +0 -216
  217. package/src/validate.test.ts +0 -287
  218. package/src/validate.ts +0 -307
  219. package/src/validation-attributes.ts +0 -43
  220. package/src/validation.test.ts +0 -330
  221. package/src/validation.ts +0 -246
  222. package/src/variable-name-inference.test.ts +0 -178
  223. package/src/variable-name-inference.ts +0 -242
  224. package/src/webhook.test.ts +0 -649
  225. package/src/webhook.ts +0 -637
  226. package/src/workflow-distributed.test.ts +0 -786
  227. package/src/workflow-distributed.ts +0 -916
  228. package/src/workflow.async-safety.integration.test.ts +0 -345
  229. package/src/workflow.test.ts +0 -647
  230. package/src/workflow.ts +0 -810
  231. package/src/yaml-config.test.ts +0 -337
  232. package/src/yaml-config.ts +0 -342
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;
@@ -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
- });