autotel-audit 0.4.3 → 0.4.5

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/LICENSE CHANGED
@@ -1,21 +1,191 @@
1
- MIT License
2
-
3
- Copyright (c) Jag Reehal 2025
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction, and
10
+ distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by the
13
+ copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all other
16
+ entities that control, are controlled by, or are under common control with
17
+ that entity. For the purposes of this definition, "control" means (i) the
18
+ power, direct or indirect, to cause the direction or management of such
19
+ entity, whether by contract or otherwise, or (ii) ownership of fifty
20
+ percent (50%) or more of the outstanding shares, or (iii) beneficial
21
+ ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity exercising
24
+ permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation source,
28
+ and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical transformation
31
+ or translation of a Source form, including but not limited to compiled
32
+ object code, generated documentation, and conversions to other media types.
33
+
34
+ "Work" shall mean the work of authorship, whether in Source or Object form,
35
+ made available under the License, as indicated by a copyright notice that
36
+ is included in or attached to the work (an example is provided in the
37
+ Appendix below).
38
+
39
+ "Derivative Works" shall mean any work, whether in Source or Object form,
40
+ that is based on (or derived from) the Work and for which the editorial
41
+ revisions, annotations, elaborations, or other modifications represent, as
42
+ a whole, an original work of authorship. For the purposes of this License,
43
+ Derivative Works shall not include works that remain separable from, or
44
+ merely link (or bind by name) to the interfaces of, the Work and Derivative
45
+ Works thereof.
46
+
47
+ "Contribution" shall mean any work of authorship, including the original
48
+ version of the Work and any modifications or additions to that Work or
49
+ Derivative Works thereof, that is intentionally submitted to Licensor for
50
+ inclusion in the Work by the copyright owner or by an individual or Legal
51
+ Entity authorized to submit on behalf of the copyright owner. For the
52
+ purposes of this definition, "submitted" means any form of electronic,
53
+ verbal, or written communication sent to the Licensor or its
54
+ representatives, including but not limited to communication on electronic
55
+ mailing lists, source code control systems, and issue tracking systems that
56
+ are managed by, or on behalf of, the Licensor for the purpose of discussing
57
+ and improving the Work, but excluding communication that is conspicuously
58
+ marked or otherwise designated in writing by the copyright owner as "Not a
59
+ Contribution."
60
+
61
+ "Contributor" shall mean Licensor and any individual or Legal Entity on
62
+ behalf of whom a Contribution has been received by Licensor and
63
+ subsequently incorporated within the Work.
64
+
65
+ 2. Grant of Copyright License. Subject to the terms and conditions of this
66
+ License, each Contributor hereby grants to You a perpetual, worldwide,
67
+ non-exclusive, no-charge, royalty-free, irrevocable copyright license to
68
+ reproduce, prepare Derivative Works of, publicly display, publicly perform,
69
+ sublicense, and distribute the Work and such Derivative Works in Source or
70
+ Object form.
71
+
72
+ 3. Grant of Patent License. Subject to the terms and conditions of this
73
+ License, each Contributor hereby grants to You a perpetual, worldwide,
74
+ non-exclusive, no-charge, royalty-free, irrevocable (except as stated in
75
+ this section) patent license to make, have made, use, offer to sell, sell,
76
+ import, and otherwise transfer the Work, where such license applies only to
77
+ those patent claims licensable by such Contributor that are necessarily
78
+ infringed by their Contribution(s) alone or by combination of their
79
+ Contribution(s) with the Work to which such Contribution(s) was submitted.
80
+ If You institute patent litigation against any entity (including a
81
+ cross-claim or counterclaim in a lawsuit) alleging that the Work or a
82
+ Contribution incorporated within the Work constitutes direct or
83
+ contributory patent infringement, then any patent licenses granted to You
84
+ under this License for that Work shall terminate as of the date such
85
+ litigation is filed.
86
+
87
+ 4. Redistribution. You may reproduce and distribute copies of the Work or
88
+ Derivative Works thereof in any medium, with or without modifications, and
89
+ in Source or Object form, provided that You meet the following conditions:
90
+
91
+ (a) You must give any other recipients of the Work or Derivative Works
92
+ a copy of this License; and
93
+
94
+ (b) You must cause any modified files to carry prominent notices
95
+ stating that You changed the files; and
96
+
97
+ (c) You must retain, in the Source form of any Derivative Works that
98
+ You distribute, all copyright, patent, trademark, and attribution notices
99
+ from the Source form of the Work, excluding those notices that do not
100
+ pertain to any part of the Derivative Works; and
101
+
102
+ (d) If the Work includes a "NOTICE" text file as part of its
103
+ distribution, then any Derivative Works that You distribute must include a
104
+ readable copy of the attribution notices contained within such NOTICE file,
105
+ excluding those notices that do not pertain to any part of the Derivative
106
+ Works, in at least one of the following places: within a NOTICE text file
107
+ distributed as part of the Derivative Works; within the Source form or
108
+ documentation, if provided along with the Derivative Works; or, within a
109
+ display generated by the Derivative Works, if and wherever such third-party
110
+ notices normally appear. The contents of the NOTICE file are for
111
+ informational purposes only and do not modify the License. You may add Your
112
+ own attribution notices within Derivative Works that You distribute,
113
+ alongside or as an addendum to the NOTICE text from the Work, provided that
114
+ such additional attribution notices cannot be construed as modifying the
115
+ License.
116
+
117
+ You may add Your own copyright statement to Your modifications and may
118
+ provide additional or different license terms and conditions for use,
119
+ reproduction, or distribution of Your modifications, or for any such
120
+ Derivative Works as a whole, provided Your use, reproduction, and
121
+ distribution of the Work otherwise complies with the conditions stated in
122
+ this License.
123
+
124
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any
125
+ Contribution intentionally submitted for inclusion in the Work by You to
126
+ the Licensor shall be under the terms and conditions of this License,
127
+ without any additional terms or conditions. Notwithstanding the above,
128
+ nothing herein shall supersede or modify the terms of any separate license
129
+ agreement you may have executed with Licensor regarding such Contributions.
130
+
131
+ 6. Trademarks. This License does not grant permission to use the trade
132
+ names, trademarks, service marks, or product names of the Licensor, except
133
+ as required for reasonable and customary use in describing the origin of
134
+ the Work and reproducing the content of the NOTICE file.
135
+
136
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to
137
+ in writing, Licensor provides the Work (and each Contributor provides its
138
+ Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
139
+ KIND, either express or implied, including, without limitation, any
140
+ warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or
141
+ FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for
142
+ determining the appropriateness of using or redistributing the Work and
143
+ assume any risks associated with Your exercise of permissions under this
144
+ License.
145
+
146
+ 8. Limitation of Liability. In no event and under no legal theory, whether
147
+ in tort (including negligence), contract, or otherwise, unless required by
148
+ applicable law (such as deliberate and grossly negligent acts) or agreed to
149
+ in writing, shall any Contributor be liable to You for damages, including
150
+ any direct, indirect, special, incidental, or consequential damages of any
151
+ character arising as a result of this License or out of the use or
152
+ inability to use the Work (including but not limited to damages for loss of
153
+ goodwill, work stoppage, computer failure or malfunction, or any and all
154
+ other commercial damages or losses), even if such Contributor has been
155
+ advised of the possibility of such damages.
156
+
157
+ 9. Accepting Warranty or Additional Liability. While redistributing the
158
+ Work or Derivative Works thereof, You may choose to offer, and charge a fee
159
+ for, acceptance of support, warranty, indemnity, or other liability
160
+ obligations and/or rights consistent with this License. However, in
161
+ accepting such obligations, You may act only on Your own behalf and on Your
162
+ sole responsibility, not on behalf of any other Contributor, and only if
163
+ You agree to indemnify, defend, and hold each Contributor harmless for any
164
+ liability incurred by, or claims asserted against, such Contributor by
165
+ reason of your accepting any such warranty or additional liability.
166
+
167
+ END OF TERMS AND CONDITIONS
168
+
169
+ APPENDIX: How to apply the Apache License to your work.
170
+
171
+ To apply the Apache License to your work, attach the following boilerplate
172
+ notice, with the fields enclosed by brackets "[]" replaced with your own
173
+ identifying information. (Don't include the brackets!) The text should be
174
+ enclosed in the appropriate comment syntax for the file format. We also
175
+ recommend that a file or class name and description of purpose be included
176
+ on the same "printed page" as the copyright notice for easier
177
+ identification within third-party archives.
178
+
179
+ Copyright [yyyy] [name of copyright owner]
180
+
181
+ Licensed under the Apache License, Version 2.0 (the "License");
182
+ you may not use this file except in compliance with the License.
183
+ You may obtain a copy of the License at
184
+
185
+ http://www.apache.org/licenses/LICENSE-2.0
186
+
187
+ Unless required by applicable law or agreed to in writing, software
188
+ distributed under the License is distributed on an "AS IS" BASIS,
189
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190
+ See the License for the specific language governing permissions and
191
+ limitations under the License.
package/dist/index.cjs CHANGED
@@ -438,7 +438,8 @@ function createSecuritySignalProcessor(options = {}) {
438
438
  const traceId = readTraceId(span);
439
439
  if (!traceId) return;
440
440
  const nowMs = now();
441
- pruneExpiredUntrustedTraces(untrustedByTrace, nowMs - actionChainWindowMs);
441
+ const cutoff = nowMs - actionChainWindowMs;
442
+ pruneExpiredUntrustedTraces(untrustedByTrace, cutoff);
442
443
  if (readBooleanAttribute(span.attributes, MCP_TOOL_UNTRUSTED)) {
443
444
  const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);
444
445
  untrustedByTrace.set(traceId, {
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["otelTrace","SECURITY_ATTR","REDACTOR_PATTERNS","SECURITY_METRICS","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP","SECURITY_DENIED_STATUSES","SECURITY_METRICS","HTTP_STATUS_ATTRIBUTES","SECURITY_ATTR","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP","SECURITY_METRICS","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP"],"sources":["../src/context.ts","../src/lazy-counter.ts","../src/security.ts","../src/security-signals.ts","../src/security-heartbeat.ts","../src/mcp-bridge.ts","../src/index.ts"],"sourcesContent":["import { getTraceContext, otelTrace } from 'autotel';\n\nexport interface AuditContext {\n traceId: string;\n spanId: string;\n correlationId: string;\n setAttribute(key: string, value: string | number | boolean): void;\n setAttributes(\n attrs: Record<string, string | number | boolean | string[] | number[] | boolean[]>,\n ): void;\n}\n\nconst MISSING_CONTEXT_MESSAGE =\n '[autotel-audit] No active trace context. Wrap the call in trace()/instrument(), pass options.ctx, ' +\n 'or set options.onMissingContext to \"warn\"/\"skip\" to degrade gracefully instead of throwing.';\n\n/**\n * Resolve an audit context without throwing. Returns `null` when no trace context\n * is available, so callers can degrade gracefully (best-effort instrumentation).\n */\nconst INVALID_TRACE_ID = '00000000000000000000000000000000';\n\nexport function resolveContextSafe(ctx?: AuditContext): AuditContext | null {\n if (ctx) return ctx;\n\n const span = otelTrace.getActiveSpan();\n if (!span) return null;\n\n // Resolve trace ids from autotel's context when available, otherwise from the\n // active OTel span itself, so audit works in any OTel setup — not only inside\n // autotel's own `trace()`.\n const ids = getTraceContext();\n const sc = span.spanContext();\n const traceId = ids?.traceId ?? sc.traceId;\n if (!traceId || traceId === INVALID_TRACE_ID) return null;\n\n return {\n traceId,\n spanId: ids?.spanId ?? sc.spanId,\n correlationId: ids?.correlationId ?? traceId.slice(0, 16),\n setAttribute: (key, value) => span.setAttribute(key, value),\n setAttributes: (attrs) => span.setAttributes(attrs),\n };\n}\n\nexport function resolveContext(ctx?: AuditContext): AuditContext {\n const resolved = resolveContextSafe(ctx);\n if (resolved) return resolved;\n throw new Error(MISSING_CONTEXT_MESSAGE);\n}\n\nexport { MISSING_CONTEXT_MESSAGE };\n\n/**\n * How instrumentation should behave when no trace context is available.\n *\n * - `throw` — fail fast (original behaviour). Use when telemetry is mandatory.\n * - `warn` — run the wrapped handler un-audited and log one warning per action (default).\n * - `skip` — run the wrapped handler un-audited, silently.\n *\n * Telemetry is observability: a missing context should never crash the business\n * logic it wraps, so the default is best-effort (`warn`).\n */\nexport type OnMissingContext = 'throw' | 'warn' | 'skip';\n\n/** A no-op {@link AuditContext} whose attribute setters do nothing. */\nexport function noopAuditContext(): AuditContext {\n return {\n traceId: '',\n spanId: '',\n correlationId: '',\n setAttribute() {},\n setAttributes() {},\n };\n}\n\nconst warnedMissingContext = new Set<string>();\nconst warnedMissingLogger = new Set<string>();\n\n/** Warn (once per action) that instrumentation is running without a trace context. */\nexport function warnMissingContextOnce(action: string): void {\n if (warnedMissingContext.has(action)) return;\n warnedMissingContext.add(action);\n console.warn(\n `[autotel-audit] No active trace context for \"${action}\" — running un-audited. ` +\n 'Wrap the call in trace()/instrument() or pass options.ctx to capture telemetry. ' +\n '(set options.onMissingContext: \"throw\" to fail fast, or \"skip\" to silence this warning)',\n );\n}\n\n/** Warn (once per action) that attributes were recorded but no canonical log line emitted. */\nexport function warnMissingLoggerOnce(action: string): void {\n if (warnedMissingLogger.has(action)) return;\n warnedMissingLogger.add(action);\n console.warn(\n `[autotel-audit] No request logger for \"${action}\" — attributes were recorded on the span, ` +\n 'but no canonical log line was emitted. Pass options.logger or run inside runWithRequestContext().',\n );\n}\n\nexport function toAttributeValue(\n value: unknown,\n): string | number | boolean | string[] | number[] | boolean[] | undefined {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n return value;\n }\n\n if (Array.isArray(value)) {\n if (value.every((entry) => typeof entry === 'string')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'number')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'boolean')) {\n return value;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (value === null || value === undefined) {\n return undefined;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n}\n","import { createCounter } from 'autotel';\n\nexport interface LazyCounter {\n add(value: number, attributes?: Record<string, string | number | boolean>): void;\n}\n\n/**\n * Counter that is created on first use (the meter may not be configured\n * until `init()` completes) and whose failures are swallowed — metrics\n * must never break event emission or the span pipeline.\n */\nexport function lazyCounter(name: string, description: string): LazyCounter {\n let counter: ReturnType<typeof createCounter> | undefined;\n return {\n add(value, attributes) {\n try {\n counter ??= createCounter(name, { description });\n counter.add(value, attributes);\n } catch {\n // Swallow — observability must never take the process down.\n }\n },\n };\n}\n","import { createHash } from 'node:crypto';\nimport {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n REDACTOR_PATTERNS,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n SECURITY_ATTR,\n SECURITY_METRICS,\n escalateSecuritySeverity,\n} from 'autotel/security-schema';\nimport type { SecuritySeverity } from 'autotel/security-schema';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\nimport { lazyCounter } from './lazy-counter';\n\nexport type { SecuritySeverity };\n\n/**\n * Security event categories, aligned with OWASP A09:2025\n * (Security Logging & Alerting Failures) and ASVS V7.\n */\nexport type SecurityEventCategory =\n | 'authentication'\n | 'authorization'\n | 'data_access'\n | 'admin_action'\n | 'configuration'\n | 'secrets'\n | 'rate_limit'\n | 'validation'\n | 'supply_chain'\n | 'llm';\n\nexport type SecurityOutcome =\n | 'success'\n | 'failure'\n | 'denied'\n | 'blocked'\n | 'error';\n\n/**\n * Well-known security event names. Free-form names are allowed —\n * this union exists for autocomplete and consistency across services.\n */\nexport type SuggestedSecurityEventName =\n | 'auth.login.success'\n | 'auth.login.failed'\n | 'auth.mfa.failed'\n | 'auth.session.revoked'\n | 'auth.password.reset'\n | 'auth.account.locked'\n | 'access.denied'\n | 'access.role.changed'\n | 'access.permission.changed'\n | 'access.tenant.violation'\n | 'admin.action'\n | 'config.changed'\n | 'secret.accessed'\n | 'secret.rotation.failed'\n | 'api_key.created'\n | 'api_key.revoked'\n | 'rate_limit.exceeded'\n | 'validation.failed'\n | 'webhook.signature.failed'\n | 'dependency.scan.failed'\n | 'llm.prompt_injection.detected'\n | 'llm.tool_call.denied'\n | 'llm.output.blocked'\n | 'llm.output.budget_exceeded'\n | 'llm.guard.triggered'\n | 'llm.action_chain.suspicious'\n | 'llm.manifest.suspicious'\n | 'llm.plan.risk.elevated';\n\nexport interface SecurityEventMetadata {\n /** Stable, dot-separated event name, e.g. `auth.login.failed`. */\n name: SuggestedSecurityEventName | (string & {});\n category: SecurityEventCategory;\n outcome: SecurityOutcome;\n /** Defaults to `info`. */\n severity?: SecuritySeverity;\n /** Stable identifier of the actor — an id or a `hashIdentifier()` digest, never raw PII. */\n actorId?: string;\n targetType?: string;\n targetId?: string;\n tenantId?: string;\n /** Short machine-readable reason, e.g. `invalid_password`. */\n reason?: string;\n [key: string]: unknown;\n}\n\nexport interface SecurityEventOptions {\n ctx?: AuditContext;\n /**\n * Security events are exempt from tail sampling by default —\n * an attack you sampled away is an attack you cannot investigate.\n * Pass `false` to opt out (e.g. very high-volume info events).\n */\n forceKeep?: boolean;\n emitNow?: boolean;\n logger?: RequestLogger;\n /**\n * Also increment the `autotel.security.events` counter\n * (attributes: event, category, outcome, severity) so security teams\n * can alert on rates without log-based alerting. Default true.\n *\n * Cardinality note: the event name is a counter attribute — keep names\n * to a stable catalogue, never interpolate user input into them.\n */\n metrics?: boolean;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: record nothing, warn once). A dropped security event is still\n * better than a crashed request — but the warning makes the gap visible.\n */\n onMissingContext?: OnMissingContext;\n}\n\nexport type WithSecurityOptions = SecurityEventOptions;\n\ninterface SecurityAttributeSink {\n setAttribute(\n key: string,\n value:\n | string\n | number\n | boolean\n | string[]\n | number[]\n | boolean[],\n ): unknown;\n}\n\n/**\n * Standard metadata fields and the schema attribute each maps to.\n * Drives both standard-field emission and the reserved-key check for the\n * custom-attribute loop — adding a field here is the whole change.\n */\nconst FIELD_ATTRIBUTES: Record<string, string> = {\n name: SECURITY_ATTR.event,\n category: SECURITY_ATTR.category,\n outcome: SECURITY_ATTR.outcome,\n severity: SECURITY_ATTR.severity,\n actorId: SECURITY_ATTR.actorId,\n targetType: SECURITY_ATTR.targetType,\n targetId: SECURITY_ATTR.targetId,\n tenantId: SECURITY_ATTR.tenantId,\n reason: SECURITY_ATTR.reason,\n};\n\nfunction flattenSecurityAttributes(\n metadata: SecurityEventMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n [SECURITY_ATTR.marker]: true,\n [SECURITY_ATTR.severity]: metadata.severity ?? 'info',\n };\n\n const droppedKeys: string[] = [];\n for (const [key, value] of Object.entries(metadata)) {\n const standardAttribute = FIELD_ATTRIBUTES[key];\n // Never emit values under credential-shaped custom keys, even by\n // accident. Reuses the core redactor's sensitive-key pattern so the\n // deny-list stays in one place.\n if (\n standardAttribute === undefined &&\n REDACTOR_PATTERNS.sensitiveKey.test(key)\n ) {\n droppedKeys.push(key);\n continue;\n }\n\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[standardAttribute ?? `security.${key}`] = attr;\n }\n }\n\n if (droppedKeys.length > 0) {\n attributes[SECURITY_ATTR.droppedKeys] = droppedKeys;\n }\n\n return attributes;\n}\n\nconst eventsCounter = lazyCounter(\n SECURITY_METRICS.events,\n 'Security events by name, category, outcome, and severity',\n);\n\nfunction countSecurityEvent(metadata: SecurityEventMetadata): void {\n eventsCounter.add(1, {\n event: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n });\n}\n\nexport function applySecurityEventAttributes(\n sink: SecurityAttributeSink,\n metadata: SecurityEventMetadata,\n options: Pick<SecurityEventOptions, 'forceKeep' | 'metrics'> = {},\n): void {\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (options.forceKeep !== false) {\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n sink.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n\n for (const [key, value] of Object.entries(flattenSecurityAttributes(metadata))) {\n sink.setAttribute(key, value);\n }\n}\n\n/**\n * Record a security event on the active trace and request logger.\n *\n * Events are force-kept through tail sampling by default and carry\n * `security.*` attributes (`security.event`, `security.category`,\n * `security.outcome`, `security.severity`) so backends can build\n * detection rules and dashboards from a stable schema.\n *\n * ```typescript\n * securityEvent({\n * name: 'auth.login.failed',\n * category: 'authentication',\n * outcome: 'failure',\n * severity: 'warning',\n * actorId: hashIdentifier(email),\n * reason: 'invalid_password',\n * });\n * ```\n */\nexport function securityEvent(\n metadata: SecurityEventMetadata,\n options: SecurityEventOptions = {},\n): void {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // Counters are independent of trace context — always record the security signal\n // even when there's no span to attach attributes to.\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.name);\n }\n return;\n }\n\n if (options.forceKeep !== false) {\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n traceCtx.setAttributes(flattenSecurityAttributes(metadata));\n\n const logger = options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n logger.set({\n security: {\n name: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n ...(metadata.reason !== undefined && { reason: metadata.reason }),\n forceKeep: options.forceKeep !== false,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n}\n\n/**\n * Wrap a security-sensitive operation. On success the event outcome is\n * recorded as given (default `success`); a thrown error records\n * `outcome: 'error'`, escalates the severity to at least `error`, and\n * rethrows.\n *\n * ```typescript\n * await withSecurity(\n * { name: 'api_key.created', category: 'secrets', outcome: 'success', actorId: userId },\n * async () => createApiKey(userId),\n * );\n * ```\n */\nexport async function withSecurity<T>(\n metadata: SecurityEventMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithSecurityOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n const logger =\n options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n const ctx = traceCtx ?? noopAuditContext();\n\n try {\n const result = await fn(ctx, logger);\n securityEvent(metadata, { ...options, ctx: traceCtx ?? undefined, logger });\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n securityEvent(\n {\n ...metadata,\n outcome: 'error',\n // A failed security-sensitive operation is never less than an error,\n // but an explicit `critical` stays critical.\n severity: escalateSecuritySeverity(metadata.severity ?? 'info', 'error'),\n },\n { ...options, ctx: traceCtx ?? undefined, logger },\n );\n logger.error(asError, {\n security: {\n name: metadata.name,\n category: metadata.category,\n },\n });\n throw asError;\n }\n}\n\nexport interface HashIdentifierOptions {\n /** Optional salt; use one stable per-deployment salt to defeat rainbow lookups. */\n salt?: string;\n /** Digest length in hex chars (default 16). */\n length?: number;\n}\n\n/**\n * Stable one-way digest for correlating PII-bearing identifiers\n * (emails, IPs) across events WITHOUT logging the raw value.\n *\n * NOT for secrets — never log secrets in any form, hashed or not.\n */\nexport function hashIdentifier(\n value: string,\n options: HashIdentifierOptions = {},\n): string {\n const length = options.length ?? 16;\n return createHash('sha256')\n .update(options.salt ? `${options.salt}:${value}` : value)\n .digest('hex')\n .slice(0, length);\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n} from 'autotel';\nimport {\n HTTP_STATUS_ATTRIBUTES,\n SECURITY_ATTR,\n SECURITY_DENIED_STATUSES,\n SECURITY_METRICS,\n} from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\nimport { applySecurityEventAttributes } from './security.js';\n\n/**\n * Zero-code security signal derivation from spans you already have.\n *\n * `createSecuritySignalProcessor()` watches ordinary HTTP server spans and:\n *\n * - flags suspicious request paths (traversal, `.env`/`.git` probes,\n * SQLi/XSS probes) at span start, marking them `security.suspicious_request`\n * and force-keeping them through tail sampling\n * - counts denied responses (401/403/429 by default) into the\n * `autotel.security.http.denied` metric\n * - detects auth-failure bursts per client (sliding window) and surfaces\n * them via the `autotel.security.anomaly` metric and an `onSignal` callback\n *\n * ```typescript\n * init({\n * service: 'api',\n * spanProcessors: [createSecuritySignalProcessor()],\n * });\n * ```\n *\n * Detection rules, alert thresholds, and dashboards belong in your\n * observability backend — this processor's job is to make the signals\n * exist, survive sampling, and stay queryable under a stable schema.\n */\n\n// Structural subset of @opentelemetry/sdk-trace-base types — kept local so\n// autotel-audit adds no new dependencies. Objects returned here satisfy the\n// real SpanProcessor interface structurally (must mirror @opentelemetry/api's\n// AttributeValue, including nullable array entries).\ntype AttributeValue =\n | string\n | number\n | boolean\n | Array<null | undefined | string>\n | Array<null | undefined | number>\n | Array<null | undefined | boolean>;\n\ninterface MutableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n setAttribute(key: string, value: AttributeValue): unknown;\n}\n\ninterface ReadableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n}\n\nexport interface SecuritySignalProcessor {\n onStart(span: MutableSpanLike, parentContext?: unknown): void;\n onEnd(span: ReadableSpanLike): void;\n shutdown(): Promise<void>;\n forceFlush(): Promise<void>;\n}\n\nexport interface SuspiciousRequestSignal {\n signal: 'suspicious_request';\n /** Which pattern matched, e.g. `path_traversal`. */\n pattern: string;\n /** The matched request path/URL (as found on the span). */\n target: string;\n}\n\nexport interface AuthFailureBurstSignal {\n signal: 'auth_failure_burst';\n /** Value of the configured key attribute (e.g. client address). */\n key: string;\n /** Denied responses observed inside the window. */\n count: number;\n windowMs: number;\n status: number;\n}\n\nexport interface LlmExcessiveTokensSignal {\n signal: 'llm_excessive_tokens';\n /** Total tokens consumed by the single LLM call. */\n tokens: number;\n maxTokens: number;\n model?: string;\n}\n\nexport interface LlmTokenBudgetSignal {\n signal: 'llm_token_budget_exceeded';\n /** Value of the configured key attribute (e.g. end-user id). */\n key: string;\n /** Tokens consumed inside the window. */\n tokens: number;\n budget: number;\n windowMs: number;\n}\n\nexport interface LlmActionChainSuspiciousSignal {\n signal: 'llm_action_chain_suspicious';\n /** Trace where the suspicious chain was observed. */\n traceId: string;\n /** Tool that followed untrusted-content processing. */\n toolName?: string;\n /** Milliseconds since the untrusted tool call on the same trace. */\n elapsedMs: number;\n untrustedTool?: string;\n}\n\nexport type SecuritySignal =\n | SuspiciousRequestSignal\n | AuthFailureBurstSignal\n | LlmExcessiveTokensSignal\n | LlmTokenBudgetSignal\n | LlmActionChainSuspiciousSignal;\n\nexport interface BurstOptions {\n /** HTTP statuses counted toward a burst. Default `[401, 403]`. */\n statuses?: number[];\n /** Denied responses within the window that trigger a signal. Default 10. */\n threshold?: number;\n /** Sliding window size in milliseconds. Default 60_000. */\n windowMs?: number;\n /**\n * Span attribute identifying the client. Default `client.address`\n * (falls back to `http.client_ip`).\n */\n keyAttribute?: string;\n /** Max distinct clients tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n}\n\nexport interface LlmSignalOptions {\n /**\n * Single-call token ceiling (`gen_ai.usage.total_tokens`, or input+output).\n * Default 100_000. Pass `false` to disable the per-call check.\n */\n maxTokensPerCall?: number | false;\n /**\n * Sliding-window token budget per key — catches slow-drip abuse that\n * stays under the per-call ceiling (OWASP LLM10: Unbounded Consumption).\n * Off unless configured.\n */\n tokenBudget?: {\n budget: number;\n /** Window size in milliseconds. Default 300_000 (5 min). */\n windowMs?: number;\n /**\n * Span attribute identifying the consumer. Default `enduser.id`\n * (falls back to `client.address`).\n */\n keyAttribute?: string;\n /** Max distinct keys tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n };\n}\n\nexport interface SecuritySignalProcessorOptions {\n /** Flag suspicious request paths on span start. Default true. */\n detectSuspiciousRequests?: boolean;\n /** Additional name → pattern pairs checked against the request target. */\n extraPatterns?: Record<string, RegExp>;\n /** Force-keep flagged spans through tail sampling. Default true. */\n forceKeepSuspicious?: boolean;\n /** HTTP statuses counted as denied. Default `[401, 403, 429]`. */\n deniedStatuses?: number[];\n /** Burst detection over denied responses. Pass `false` to disable. */\n burst?: BurstOptions | false;\n /**\n * LLM consumption signals from `gen_ai.*` spans (OWASP LLM10).\n * Enabled with the per-call ceiling by default; pass `false` to disable.\n */\n llm?: LlmSignalOptions | false;\n /**\n * Detect destructive MCP tool calls that follow untrusted-content tool usage\n * on the same trace (Google's \"read email then send externally\" pattern).\n * Default true.\n */\n detectSuspiciousActionChains?: boolean;\n /** Max ms between untrusted and destructive tool calls on one trace. Default 300_000. */\n actionChainWindowMs?: number;\n /** Emit `autotel.security.*` metrics. Default true. */\n metrics?: boolean;\n /** Called whenever a signal fires. Keep it fast and non-throwing. */\n onSignal?: (signal: SecuritySignal) => void;\n /** Clock override for tests. */\n now?: () => number;\n}\n\n/**\n * Conservative request-target patterns. Tuned for scanner/probe traffic —\n * high signal, low false-positive — not as a WAF. Extend via `extraPatterns`.\n */\nexport const SUSPICIOUS_REQUEST_PATTERNS: Record<string, RegExp> = {\n path_traversal: /(\\.\\.[/\\\\]|%2e%2e(%2f|%5c|\\/)|\\.\\.%2f|%252e%252e)/i,\n sensitive_file_probe:\n /(\\/\\.env\\b|\\/\\.git\\b|\\/etc\\/passwd|\\/wp-admin\\b|\\/\\.aws\\b|\\/id_rsa\\b)/i,\n sqli_probe:\n /(\\bunion\\b[\\s+%20]+(all[\\s+%20]+)?select\\b|'[\\s+%20]*or[\\s+%20]*'?1'?[\\s+%20]*=[\\s+%20]*'?1)/i,\n xss_probe: /(<script\\b|%3cscript)/i,\n null_byte: /%00/,\n};\n\nconst TARGET_ATTRIBUTES = [\n 'url.path',\n 'url.full',\n 'http.target',\n 'http.url',\n] as const;\n\nfunction readAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n keys: readonly string[],\n): AttributeValue | undefined {\n for (const key of keys) {\n const value = attributes[key];\n if (value !== undefined) return value;\n }\n return undefined;\n}\n\n/**\n * Weighted sliding-window counter with bounded key cardinality.\n * Weight 1 per hit counts occurrences; token counts as weights sum usage.\n */\nclass SlidingWindow {\n private readonly hits = new Map<string, Array<[number, number]>>();\n\n constructor(\n private readonly windowMs: number,\n private readonly maxKeys: number,\n ) {}\n\n /**\n * Record a hit; returns the totals inside the window before and after it,\n * so callers can signal exactly once on a threshold crossing.\n */\n record(key: string, now: number, weight = 1): { before: number; after: number } {\n let entries = this.hits.get(key);\n if (!entries) {\n // Bound memory: random client addresses must not grow the map forever.\n if (this.hits.size >= this.maxKeys) {\n const oldest = this.hits.keys().next().value;\n if (oldest !== undefined) this.hits.delete(oldest);\n }\n entries = [];\n this.hits.set(key, entries);\n }\n\n const cutoff = now - this.windowMs;\n while (entries.length > 0 && (entries[0] as [number, number])[0] < cutoff) {\n entries.shift();\n }\n\n let before = 0;\n for (const [, w] of entries) before += w;\n entries.push([now, weight]);\n return { before, after: before + weight };\n }\n}\n\n/** Burst detection options with defaults applied and the window attached. */\ninterface BurstConfig {\n statuses: Set<number>;\n threshold: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n}\n\nfunction resolveBurstConfig(\n option: BurstOptions | false | undefined,\n): BurstConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const windowMs = opts.windowMs ?? 60_000;\n return {\n statuses: new Set(opts.statuses ?? [401, 403]),\n threshold: opts.threshold ?? 10,\n windowMs,\n keyAttribute: opts.keyAttribute ?? 'client.address',\n window: new SlidingWindow(windowMs, opts.maxKeys ?? 10_000),\n };\n}\n\n/** LLM consumption options with defaults applied and windows attached. */\ninterface LlmConfig {\n maxTokensPerCall?: number;\n budget?: {\n budget: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n };\n}\n\nfunction resolveLlmConfig(\n option: LlmSignalOptions | false | undefined,\n): LlmConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const tokenBudget = opts.tokenBudget;\n const windowMs = tokenBudget?.windowMs ?? 300_000;\n return {\n maxTokensPerCall:\n opts.maxTokensPerCall === false\n ? undefined\n : (opts.maxTokensPerCall ?? 100_000),\n budget: tokenBudget && {\n budget: tokenBudget.budget,\n windowMs,\n keyAttribute: tokenBudget.keyAttribute ?? 'enduser.id',\n window: new SlidingWindow(windowMs, tokenBudget.maxKeys ?? 10_000),\n },\n };\n}\n\nconst MCP_TOOL_UNTRUSTED = 'mcp.tool.untrusted_content';\nconst MCP_TOOL_DESTRUCTIVE = 'mcp.tool.destructive';\nconst MCP_TOOL_NAME = 'mcp.tool.name';\n\ninterface UntrustedToolHit {\n toolName?: string;\n timestamp: number;\n}\n\nfunction pruneExpiredUntrustedTraces(\n hits: Map<string, UntrustedToolHit>,\n cutoff: number,\n): void {\n for (const [traceId, hit] of hits) {\n if (hit.timestamp < cutoff) {\n hits.delete(traceId);\n }\n }\n}\n\nfunction readTraceId(span: ReadableSpanLike): string | undefined {\n const fromContext = span.spanContext?.traceId;\n if (typeof fromContext === 'string' && fromContext.length > 0) {\n return fromContext;\n }\n const fromAttr = readAttribute(span.attributes, ['trace_id']);\n return typeof fromAttr === 'string' && fromAttr.length > 0 ? fromAttr : undefined;\n}\n\nfunction readBooleanAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n key: string,\n): boolean {\n return readAttribute(attributes, [key]) === true;\n}\n\nexport function createSecuritySignalProcessor(\n options: SecuritySignalProcessorOptions = {},\n): SecuritySignalProcessor {\n const detect = options.detectSuspiciousRequests !== false;\n const forceKeep = options.forceKeepSuspicious !== false;\n const metricsEnabled = options.metrics !== false;\n const deniedStatuses = new Set(\n options.deniedStatuses ?? SECURITY_DENIED_STATUSES,\n );\n const now = options.now ?? Date.now;\n\n const patterns: Record<string, RegExp> = {\n ...SUSPICIOUS_REQUEST_PATTERNS,\n ...options.extraPatterns,\n };\n\n const burst = resolveBurstConfig(options.burst);\n const llm = resolveLlmConfig(options.llm);\n const detectActionChains = options.detectSuspiciousActionChains !== false;\n const actionChainWindowMs = options.actionChainWindowMs ?? 300_000;\n const untrustedByTrace = new Map<string, UntrustedToolHit>();\n\n const counters = {\n suspicious: lazyCounter(\n SECURITY_METRICS.httpSuspicious,\n 'Requests matching suspicious-path patterns',\n ),\n denied: lazyCounter(\n SECURITY_METRICS.httpDenied,\n 'HTTP responses with denied status codes (401/403/429)',\n ),\n anomaly: lazyCounter(\n SECURITY_METRICS.anomaly,\n 'Security anomaly signals (e.g. auth-failure bursts)',\n ),\n };\n\n function count(\n which: keyof typeof counters,\n attributes: Record<string, string | number>,\n ): void {\n if (!metricsEnabled) return;\n counters[which].add(1, attributes);\n }\n\n function emit(signal: SecuritySignal): void {\n try {\n options.onSignal?.(signal);\n } catch {\n // Callbacks must never break the span pipeline.\n }\n }\n\n function checkDeniedResponse(span: ReadableSpanLike): void {\n const status = readAttribute(span.attributes, HTTP_STATUS_ATTRIBUTES);\n if (typeof status !== 'number' || !deniedStatuses.has(status)) return;\n\n count('denied', { status });\n\n if (!burst || !burst.statuses.has(status)) return;\n\n const key = readAttribute(span.attributes, [\n burst.keyAttribute,\n 'http.client_ip',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = burst.window.record(key, now());\n // Signal once per window on the exact crossing, not on every\n // subsequent hit — keeps anomaly volume bounded under attack.\n if (before < burst.threshold && after >= burst.threshold) {\n count('anomaly', { signal: 'auth_failure_burst', status });\n emit({\n signal: 'auth_failure_burst',\n key,\n count: after,\n windowMs: burst.windowMs,\n status,\n });\n }\n }\n\n function checkLlmConsumption(span: ReadableSpanLike): void {\n if (!llm) return;\n\n const total = readAttribute(span.attributes, ['gen_ai.usage.total_tokens']);\n let tokens: number | undefined;\n if (typeof total === 'number') {\n tokens = total;\n } else {\n const input = readAttribute(span.attributes, ['gen_ai.usage.input_tokens']);\n const output = readAttribute(span.attributes, [\n 'gen_ai.usage.output_tokens',\n ]);\n if (typeof input === 'number' || typeof output === 'number') {\n tokens =\n (typeof input === 'number' ? input : 0) +\n (typeof output === 'number' ? output : 0);\n }\n }\n if (tokens === undefined || tokens <= 0) return;\n\n if (llm.maxTokensPerCall !== undefined && tokens > llm.maxTokensPerCall) {\n const model = readAttribute(span.attributes, [\n 'gen_ai.response.model',\n 'gen_ai.request.model',\n ]);\n count('anomaly', { signal: 'llm_excessive_tokens' });\n emit({\n signal: 'llm_excessive_tokens',\n tokens,\n maxTokens: llm.maxTokensPerCall,\n ...(typeof model === 'string' && { model }),\n });\n }\n\n const budget = llm.budget;\n if (!budget) return;\n\n const key = readAttribute(span.attributes, [\n budget.keyAttribute,\n 'client.address',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = budget.window.record(key, now(), tokens);\n if (before < budget.budget && after >= budget.budget) {\n count('anomaly', { signal: 'llm_token_budget_exceeded' });\n emit({\n signal: 'llm_token_budget_exceeded',\n key,\n tokens: after,\n budget: budget.budget,\n windowMs: budget.windowMs,\n });\n }\n }\n\n function checkSuspiciousActionChain(span: MutableSpanLike): void {\n if (!detectActionChains) return;\n\n const traceId = readTraceId(span);\n if (!traceId) return;\n\n const nowMs = now();\n const cutoff = nowMs - actionChainWindowMs;\n pruneExpiredUntrustedTraces(untrustedByTrace, cutoff);\n\n if (readBooleanAttribute(span.attributes, MCP_TOOL_UNTRUSTED)) {\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n untrustedByTrace.set(traceId, {\n timestamp: nowMs,\n ...(typeof toolName === 'string' && { toolName }),\n });\n return;\n }\n\n if (!readBooleanAttribute(span.attributes, MCP_TOOL_DESTRUCTIVE)) {\n return;\n }\n\n const prior = untrustedByTrace.get(traceId);\n if (!prior) {\n return;\n }\n untrustedByTrace.delete(traceId);\n\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n const elapsedMs = nowMs - prior.timestamp;\n count('anomaly', { signal: 'llm_action_chain_suspicious' });\n emit({\n signal: 'llm_action_chain_suspicious',\n traceId,\n elapsedMs,\n ...(typeof toolName === 'string' && { toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n });\n applySecurityEventAttributes(\n span,\n {\n name: 'llm.action_chain.suspicious',\n category: 'llm',\n outcome: 'denied',\n severity: 'warning',\n reason: 'untrusted_then_destructive',\n targetType: 'trace',\n targetId: traceId,\n ...(typeof toolName === 'string' && { destructiveTool: toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n elapsedMs,\n },\n { forceKeep: true, metrics: false },\n );\n }\n\n return {\n onStart(span) {\n checkSuspiciousActionChain(span);\n if (!detect) return;\n\n const target = readAttribute(span.attributes, TARGET_ATTRIBUTES);\n if (typeof target !== 'string' || target.length === 0) return;\n\n for (const [name, pattern] of Object.entries(patterns)) {\n if (!pattern.test(target)) continue;\n\n span.setAttribute(SECURITY_ATTR.suspiciousRequest, true);\n span.setAttribute(SECURITY_ATTR.signal, name);\n if (forceKeep) {\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n }\n\n count('suspicious', { pattern: name });\n emit({ signal: 'suspicious_request', pattern: name, target });\n return; // first match wins — one signal per span\n }\n },\n\n onEnd(span) {\n checkDeniedResponse(span);\n checkLlmConsumption(span);\n },\n\n shutdown() {\n return Promise.resolve();\n },\n\n forceFlush() {\n return Promise.resolve();\n },\n };\n}\n","import { SECURITY_METRICS } from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\n\n/**\n * Security-telemetry heartbeat.\n *\n * A silently-dead telemetry pipeline is itself a security failure (NIST\n * SP 800-92: systems must not keep operating without visibility into\n * security events). `startSecurityHeartbeat()` emits the\n * `autotel.security.heartbeat` counter on a fixed interval so security\n * teams can alert on the ABSENCE of telemetry from a service:\n *\n * ```promql\n * absent(rate(autotel_security_heartbeat_total{service_name=\"api\"}[5m]))\n * ```\n *\n * ```typescript\n * const heartbeat = startSecurityHeartbeat();\n * // on shutdown:\n * heartbeat.stop();\n * ```\n */\n\nexport interface SecurityHeartbeatOptions {\n /** Beat interval in milliseconds. Default 60_000. */\n intervalMs?: number;\n /** Extra counter attributes (keep cardinality low — labels, not data). */\n attributes?: Record<string, string | number | boolean>;\n}\n\nexport interface SecurityHeartbeat {\n stop(): void;\n}\n\nexport function startSecurityHeartbeat(\n options: SecurityHeartbeatOptions = {},\n): SecurityHeartbeat {\n const intervalMs = options.intervalMs ?? 60_000;\n const attributes = options.attributes ?? {};\n\n const counter = lazyCounter(\n SECURITY_METRICS.heartbeat,\n 'Security-telemetry liveness signal — alert on its absence',\n );\n\n function beat(): void {\n counter.add(1, attributes);\n }\n\n beat(); // establish the series immediately, not one interval later\n const timer = setInterval(beat, intervalMs);\n // Never hold the process open just to beat.\n timer.unref?.();\n\n let stopped = false;\n return {\n stop() {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n },\n };\n}\n","import type { AuditContext } from './context.js';\nimport {\n securityEvent,\n type SecurityEventMetadata,\n type SecurityEventOptions,\n} from './security.js';\n\n/**\n * Metadata emitted when MCP protocol-boundary signals are bridged to the\n * unified `security.*` schema. Used by `autotel-mcp-instrumentation` when\n * `bridgeSecurityEvents` is enabled.\n */\nexport interface McpBridgedSecurityEvent {\n name: SecurityEventMetadata['name'];\n category: 'llm';\n outcome: SecurityEventMetadata['outcome'];\n severity?: SecurityEventMetadata['severity'];\n reason?: string;\n toolName?: string;\n verdict?: string;\n source?: string;\n [key: string]: unknown;\n}\n\nexport interface McpSecurityEventBridgeOptions extends SecurityEventOptions {\n /** Optional fixed audit context for bridged events. */\n ctx?: AuditContext;\n}\n\n/**\n * Create a bridge callback for MCP security observability → `securityEvent()`.\n *\n * @example\n * ```typescript\n * import { createMcpSecurityEventBridge } from 'autotel-audit';\n * import { instrumentMcpClient } from 'autotel-mcp-instrumentation/client';\n *\n * instrumentMcpClient(client, {\n * securityClassifier: heuristicInjectionClassifier(),\n * bridgeSecurityEvents: true,\n * securityEventBridge: createMcpSecurityEventBridge(),\n * });\n * ```\n */\nexport function createMcpSecurityEventBridge(\n options: McpSecurityEventBridgeOptions = {},\n): (metadata: McpBridgedSecurityEvent) => void {\n return (metadata) => {\n const { toolName, verdict, source, ...rest } = metadata;\n securityEvent(\n {\n ...rest,\n category: 'llm',\n ...(toolName !== undefined && { targetType: 'tool', targetId: toolName }),\n ...(verdict !== undefined && { verdict }),\n ...(source !== undefined && { source }),\n },\n options,\n );\n };\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n warnMissingLoggerOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\n\nexport type { AuditContext, OnMissingContext } from './context';\nexport * from './security';\nexport * from './security-signals';\nexport * from './security-heartbeat';\nexport * from './mcp-bridge';\n\nexport interface AuditMetadata {\n action: string;\n resource?: string;\n actorId?: string;\n category?: string;\n outcome?: 'success' | 'failure' | (string & {});\n [key: string]: unknown;\n}\n\nexport interface WithAuditOptions {\n ctx?: AuditContext;\n emitNow?: boolean;\n forceKeep?: boolean;\n logger?: RequestLogger;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: run un-audited, warn once). See {@link OnMissingContext}.\n */\n onMissingContext?: OnMissingContext;\n}\n\nfunction flattenAuditAttributes(\n metadata: AuditMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n 'autotel.audit': true,\n };\n\n for (const [key, value] of Object.entries(metadata)) {\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[`audit.${key}`] = attr;\n }\n }\n\n return attributes;\n}\n\nexport function forceKeepAuditEvent(ctx?: AuditContext): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute('autotel.audit.force_keep', true);\n}\n\nexport function setAuditAttributes(\n metadata: AuditMetadata,\n ctx?: AuditContext,\n): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttributes(flattenAuditAttributes(metadata));\n}\n\nexport async function withAudit<T>(\n metadata: AuditMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithAuditOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // No trace context: degrade per onMissingContext instead of throwing into\n // business logic. Audit is observability — it must never crash the caller.\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.action);\n }\n return fn(noopAuditContext(), options.logger ?? createNoopRequestLogger());\n }\n\n if (options.forceKeep !== false) {\n forceKeepAuditEvent(traceCtx);\n }\n\n setAuditAttributes(metadata, traceCtx);\n\n // A trace context may exist (e.g. caller-supplied options.ctx) without a\n // resolvable request logger. Record span attributes regardless and only skip\n // the canonical log line — never throw.\n let logger = options.logger ?? getRequestLoggerSafe() ?? undefined;\n if (!logger) {\n if ((options.onMissingContext ?? 'warn') === 'warn') {\n warnMissingLoggerOnce(metadata.action);\n }\n logger = createNoopRequestLogger();\n }\n logger.set({\n audit: {\n ...metadata,\n forceKeep: options.forceKeep !== false,\n },\n });\n\n try {\n const result = await fn(traceCtx, logger);\n\n if (!metadata.outcome) {\n setAuditAttributes({ ...metadata, outcome: 'success' }, traceCtx);\n }\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n setAuditAttributes({ ...metadata, outcome: 'failure' }, traceCtx);\n logger.error(asError, {\n audit: {\n action: metadata.action,\n resource: metadata.resource,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n throw asError;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["otelTrace","SECURITY_ATTR","REDACTOR_PATTERNS","SECURITY_METRICS","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP","SECURITY_DENIED_STATUSES","SECURITY_METRICS","HTTP_STATUS_ATTRIBUTES","SECURITY_ATTR","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP","SECURITY_METRICS","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP"],"sources":["../src/context.ts","../src/lazy-counter.ts","../src/security.ts","../src/security-signals.ts","../src/security-heartbeat.ts","../src/mcp-bridge.ts","../src/index.ts"],"sourcesContent":["import { getTraceContext, otelTrace } from 'autotel';\n\nexport interface AuditContext {\n traceId: string;\n spanId: string;\n correlationId: string;\n setAttribute(key: string, value: string | number | boolean): void;\n setAttributes(\n attrs: Record<string, string | number | boolean | string[] | number[] | boolean[]>,\n ): void;\n}\n\nconst MISSING_CONTEXT_MESSAGE =\n '[autotel-audit] No active trace context. Wrap the call in trace()/instrument(), pass options.ctx, ' +\n 'or set options.onMissingContext to \"warn\"/\"skip\" to degrade gracefully instead of throwing.';\n\n/**\n * Resolve an audit context without throwing. Returns `null` when no trace context\n * is available, so callers can degrade gracefully (best-effort instrumentation).\n */\nconst INVALID_TRACE_ID = '00000000000000000000000000000000';\n\nexport function resolveContextSafe(ctx?: AuditContext): AuditContext | null {\n if (ctx) return ctx;\n\n const span = otelTrace.getActiveSpan();\n if (!span) return null;\n\n // Resolve trace ids from autotel's context when available, otherwise from the\n // active OTel span itself, so audit works in any OTel setup — not only inside\n // autotel's own `trace()`.\n const ids = getTraceContext();\n const sc = span.spanContext();\n const traceId = ids?.traceId ?? sc.traceId;\n if (!traceId || traceId === INVALID_TRACE_ID) return null;\n\n return {\n traceId,\n spanId: ids?.spanId ?? sc.spanId,\n correlationId: ids?.correlationId ?? traceId.slice(0, 16),\n setAttribute: (key, value) => span.setAttribute(key, value),\n setAttributes: (attrs) => span.setAttributes(attrs),\n };\n}\n\nexport function resolveContext(ctx?: AuditContext): AuditContext {\n const resolved = resolveContextSafe(ctx);\n if (resolved) return resolved;\n throw new Error(MISSING_CONTEXT_MESSAGE);\n}\n\nexport { MISSING_CONTEXT_MESSAGE };\n\n/**\n * How instrumentation should behave when no trace context is available.\n *\n * - `throw` — fail fast (original behaviour). Use when telemetry is mandatory.\n * - `warn` — run the wrapped handler un-audited and log one warning per action (default).\n * - `skip` — run the wrapped handler un-audited, silently.\n *\n * Telemetry is observability: a missing context should never crash the business\n * logic it wraps, so the default is best-effort (`warn`).\n */\nexport type OnMissingContext = 'throw' | 'warn' | 'skip';\n\n/** A no-op {@link AuditContext} whose attribute setters do nothing. */\nexport function noopAuditContext(): AuditContext {\n return {\n traceId: '',\n spanId: '',\n correlationId: '',\n setAttribute() {},\n setAttributes() {},\n };\n}\n\nconst warnedMissingContext = new Set<string>();\nconst warnedMissingLogger = new Set<string>();\n\n/** Warn (once per action) that instrumentation is running without a trace context. */\nexport function warnMissingContextOnce(action: string): void {\n if (warnedMissingContext.has(action)) return;\n warnedMissingContext.add(action);\n console.warn(\n `[autotel-audit] No active trace context for \"${action}\" — running un-audited. ` +\n 'Wrap the call in trace()/instrument() or pass options.ctx to capture telemetry. ' +\n '(set options.onMissingContext: \"throw\" to fail fast, or \"skip\" to silence this warning)',\n );\n}\n\n/** Warn (once per action) that attributes were recorded but no canonical log line emitted. */\nexport function warnMissingLoggerOnce(action: string): void {\n if (warnedMissingLogger.has(action)) return;\n warnedMissingLogger.add(action);\n console.warn(\n `[autotel-audit] No request logger for \"${action}\" — attributes were recorded on the span, ` +\n 'but no canonical log line was emitted. Pass options.logger or run inside runWithRequestContext().',\n );\n}\n\nexport function toAttributeValue(\n value: unknown,\n): string | number | boolean | string[] | number[] | boolean[] | undefined {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n return value;\n }\n\n if (Array.isArray(value)) {\n if (value.every((entry) => typeof entry === 'string')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'number')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'boolean')) {\n return value;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (value === null || value === undefined) {\n return undefined;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n}\n","import { createCounter } from 'autotel';\n\nexport interface LazyCounter {\n add(value: number, attributes?: Record<string, string | number | boolean>): void;\n}\n\n/**\n * Counter that is created on first use (the meter may not be configured\n * until `init()` completes) and whose failures are swallowed — metrics\n * must never break event emission or the span pipeline.\n */\nexport function lazyCounter(name: string, description: string): LazyCounter {\n let counter: ReturnType<typeof createCounter> | undefined;\n return {\n add(value, attributes) {\n try {\n counter ??= createCounter(name, { description });\n counter.add(value, attributes);\n } catch {\n // Swallow — observability must never take the process down.\n }\n },\n };\n}\n","import { createHash } from 'node:crypto';\nimport {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n REDACTOR_PATTERNS,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n SECURITY_ATTR,\n SECURITY_METRICS,\n escalateSecuritySeverity,\n} from 'autotel/security-schema';\nimport type { SecuritySeverity } from 'autotel/security-schema';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\nimport { lazyCounter } from './lazy-counter';\n\nexport type { SecuritySeverity };\n\n/**\n * Security event categories, aligned with OWASP A09:2025\n * (Security Logging & Alerting Failures) and ASVS V7.\n */\nexport type SecurityEventCategory =\n | 'authentication'\n | 'authorization'\n | 'data_access'\n | 'admin_action'\n | 'configuration'\n | 'secrets'\n | 'rate_limit'\n | 'validation'\n | 'supply_chain'\n | 'llm';\n\nexport type SecurityOutcome =\n | 'success'\n | 'failure'\n | 'denied'\n | 'blocked'\n | 'error';\n\n/**\n * Well-known security event names. Free-form names are allowed —\n * this union exists for autocomplete and consistency across services.\n */\nexport type SuggestedSecurityEventName =\n | 'auth.login.success'\n | 'auth.login.failed'\n | 'auth.mfa.failed'\n | 'auth.session.revoked'\n | 'auth.password.reset'\n | 'auth.account.locked'\n | 'access.denied'\n | 'access.role.changed'\n | 'access.permission.changed'\n | 'access.tenant.violation'\n | 'admin.action'\n | 'config.changed'\n | 'secret.accessed'\n | 'secret.rotation.failed'\n | 'api_key.created'\n | 'api_key.revoked'\n | 'rate_limit.exceeded'\n | 'validation.failed'\n | 'webhook.signature.failed'\n | 'dependency.scan.failed'\n | 'llm.prompt_injection.detected'\n | 'llm.tool_call.denied'\n | 'llm.output.blocked'\n | 'llm.output.budget_exceeded'\n | 'llm.guard.triggered'\n | 'llm.action_chain.suspicious'\n | 'llm.manifest.suspicious'\n | 'llm.plan.risk.elevated';\n\nexport interface SecurityEventMetadata {\n /** Stable, dot-separated event name, e.g. `auth.login.failed`. */\n name: SuggestedSecurityEventName | (string & {});\n category: SecurityEventCategory;\n outcome: SecurityOutcome;\n /** Defaults to `info`. */\n severity?: SecuritySeverity;\n /** Stable identifier of the actor — an id or a `hashIdentifier()` digest, never raw PII. */\n actorId?: string;\n targetType?: string;\n targetId?: string;\n tenantId?: string;\n /** Short machine-readable reason, e.g. `invalid_password`. */\n reason?: string;\n [key: string]: unknown;\n}\n\nexport interface SecurityEventOptions {\n ctx?: AuditContext;\n /**\n * Security events are exempt from tail sampling by default —\n * an attack you sampled away is an attack you cannot investigate.\n * Pass `false` to opt out (e.g. very high-volume info events).\n */\n forceKeep?: boolean;\n emitNow?: boolean;\n logger?: RequestLogger;\n /**\n * Also increment the `autotel.security.events` counter\n * (attributes: event, category, outcome, severity) so security teams\n * can alert on rates without log-based alerting. Default true.\n *\n * Cardinality note: the event name is a counter attribute — keep names\n * to a stable catalogue, never interpolate user input into them.\n */\n metrics?: boolean;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: record nothing, warn once). A dropped security event is still\n * better than a crashed request — but the warning makes the gap visible.\n */\n onMissingContext?: OnMissingContext;\n}\n\nexport type WithSecurityOptions = SecurityEventOptions;\n\ninterface SecurityAttributeSink {\n setAttribute(\n key: string,\n value:\n | string\n | number\n | boolean\n | string[]\n | number[]\n | boolean[],\n ): unknown;\n}\n\n/**\n * Standard metadata fields and the schema attribute each maps to.\n * Drives both standard-field emission and the reserved-key check for the\n * custom-attribute loop — adding a field here is the whole change.\n */\nconst FIELD_ATTRIBUTES: Record<string, string> = {\n name: SECURITY_ATTR.event,\n category: SECURITY_ATTR.category,\n outcome: SECURITY_ATTR.outcome,\n severity: SECURITY_ATTR.severity,\n actorId: SECURITY_ATTR.actorId,\n targetType: SECURITY_ATTR.targetType,\n targetId: SECURITY_ATTR.targetId,\n tenantId: SECURITY_ATTR.tenantId,\n reason: SECURITY_ATTR.reason,\n};\n\nfunction flattenSecurityAttributes(\n metadata: SecurityEventMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n [SECURITY_ATTR.marker]: true,\n [SECURITY_ATTR.severity]: metadata.severity ?? 'info',\n };\n\n const droppedKeys: string[] = [];\n for (const [key, value] of Object.entries(metadata)) {\n const standardAttribute = FIELD_ATTRIBUTES[key];\n // Never emit values under credential-shaped custom keys, even by\n // accident. Reuses the core redactor's sensitive-key pattern so the\n // deny-list stays in one place.\n if (\n standardAttribute === undefined &&\n REDACTOR_PATTERNS.sensitiveKey.test(key)\n ) {\n droppedKeys.push(key);\n continue;\n }\n\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[standardAttribute ?? `security.${key}`] = attr;\n }\n }\n\n if (droppedKeys.length > 0) {\n attributes[SECURITY_ATTR.droppedKeys] = droppedKeys;\n }\n\n return attributes;\n}\n\nconst eventsCounter = lazyCounter(\n SECURITY_METRICS.events,\n 'Security events by name, category, outcome, and severity',\n);\n\nfunction countSecurityEvent(metadata: SecurityEventMetadata): void {\n eventsCounter.add(1, {\n event: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n });\n}\n\nexport function applySecurityEventAttributes(\n sink: SecurityAttributeSink,\n metadata: SecurityEventMetadata,\n options: Pick<SecurityEventOptions, 'forceKeep' | 'metrics'> = {},\n): void {\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (options.forceKeep !== false) {\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n sink.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n\n for (const [key, value] of Object.entries(flattenSecurityAttributes(metadata))) {\n sink.setAttribute(key, value);\n }\n}\n\n/**\n * Record a security event on the active trace and request logger.\n *\n * Events are force-kept through tail sampling by default and carry\n * `security.*` attributes (`security.event`, `security.category`,\n * `security.outcome`, `security.severity`) so backends can build\n * detection rules and dashboards from a stable schema.\n *\n * ```typescript\n * securityEvent({\n * name: 'auth.login.failed',\n * category: 'authentication',\n * outcome: 'failure',\n * severity: 'warning',\n * actorId: hashIdentifier(email),\n * reason: 'invalid_password',\n * });\n * ```\n */\nexport function securityEvent(\n metadata: SecurityEventMetadata,\n options: SecurityEventOptions = {},\n): void {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // Counters are independent of trace context — always record the security signal\n // even when there's no span to attach attributes to.\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.name);\n }\n return;\n }\n\n if (options.forceKeep !== false) {\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n traceCtx.setAttributes(flattenSecurityAttributes(metadata));\n\n const logger = options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n logger.set({\n security: {\n name: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n ...(metadata.reason !== undefined && { reason: metadata.reason }),\n forceKeep: options.forceKeep !== false,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n}\n\n/**\n * Wrap a security-sensitive operation. On success the event outcome is\n * recorded as given (default `success`); a thrown error records\n * `outcome: 'error'`, escalates the severity to at least `error`, and\n * rethrows.\n *\n * ```typescript\n * await withSecurity(\n * { name: 'api_key.created', category: 'secrets', outcome: 'success', actorId: userId },\n * async () => createApiKey(userId),\n * );\n * ```\n */\nexport async function withSecurity<T>(\n metadata: SecurityEventMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithSecurityOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n const logger =\n options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n const ctx = traceCtx ?? noopAuditContext();\n\n try {\n const result = await fn(ctx, logger);\n securityEvent(metadata, { ...options, ctx: traceCtx ?? undefined, logger });\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n securityEvent(\n {\n ...metadata,\n outcome: 'error',\n // A failed security-sensitive operation is never less than an error,\n // but an explicit `critical` stays critical.\n severity: escalateSecuritySeverity(metadata.severity ?? 'info', 'error'),\n },\n { ...options, ctx: traceCtx ?? undefined, logger },\n );\n logger.error(asError, {\n security: {\n name: metadata.name,\n category: metadata.category,\n },\n });\n throw asError;\n }\n}\n\nexport interface HashIdentifierOptions {\n /** Optional salt; use one stable per-deployment salt to defeat rainbow lookups. */\n salt?: string;\n /** Digest length in hex chars (default 16). */\n length?: number;\n}\n\n/**\n * Stable one-way digest for correlating PII-bearing identifiers\n * (emails, IPs) across events WITHOUT logging the raw value.\n *\n * NOT for secrets — never log secrets in any form, hashed or not.\n */\nexport function hashIdentifier(\n value: string,\n options: HashIdentifierOptions = {},\n): string {\n const length = options.length ?? 16;\n return createHash('sha256')\n .update(options.salt ? `${options.salt}:${value}` : value)\n .digest('hex')\n .slice(0, length);\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n} from 'autotel';\nimport {\n HTTP_STATUS_ATTRIBUTES,\n SECURITY_ATTR,\n SECURITY_DENIED_STATUSES,\n SECURITY_METRICS,\n} from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\nimport { applySecurityEventAttributes } from './security.js';\n\n/**\n * Zero-code security signal derivation from spans you already have.\n *\n * `createSecuritySignalProcessor()` watches ordinary HTTP server spans and:\n *\n * - flags suspicious request paths (traversal, `.env`/`.git` probes,\n * SQLi/XSS probes) at span start, marking them `security.suspicious_request`\n * and force-keeping them through tail sampling\n * - counts denied responses (401/403/429 by default) into the\n * `autotel.security.http.denied` metric\n * - detects auth-failure bursts per client (sliding window) and surfaces\n * them via the `autotel.security.anomaly` metric and an `onSignal` callback\n *\n * ```typescript\n * init({\n * service: 'api',\n * spanProcessors: [createSecuritySignalProcessor()],\n * });\n * ```\n *\n * Detection rules, alert thresholds, and dashboards belong in your\n * observability backend — this processor's job is to make the signals\n * exist, survive sampling, and stay queryable under a stable schema.\n */\n\n// Structural subset of @opentelemetry/sdk-trace-base types — kept local so\n// autotel-audit adds no new dependencies. Objects returned here satisfy the\n// real SpanProcessor interface structurally (must mirror @opentelemetry/api's\n// AttributeValue, including nullable array entries).\ntype AttributeValue =\n | string\n | number\n | boolean\n | Array<null | undefined | string>\n | Array<null | undefined | number>\n | Array<null | undefined | boolean>;\n\ninterface MutableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n setAttribute(key: string, value: AttributeValue): unknown;\n}\n\ninterface ReadableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n}\n\nexport interface SecuritySignalProcessor {\n onStart(span: MutableSpanLike, parentContext?: unknown): void;\n onEnd(span: ReadableSpanLike): void;\n shutdown(): Promise<void>;\n forceFlush(): Promise<void>;\n}\n\nexport interface SuspiciousRequestSignal {\n signal: 'suspicious_request';\n /** Which pattern matched, e.g. `path_traversal`. */\n pattern: string;\n /** The matched request path/URL (as found on the span). */\n target: string;\n}\n\nexport interface AuthFailureBurstSignal {\n signal: 'auth_failure_burst';\n /** Value of the configured key attribute (e.g. client address). */\n key: string;\n /** Denied responses observed inside the window. */\n count: number;\n windowMs: number;\n status: number;\n}\n\nexport interface LlmExcessiveTokensSignal {\n signal: 'llm_excessive_tokens';\n /** Total tokens consumed by the single LLM call. */\n tokens: number;\n maxTokens: number;\n model?: string;\n}\n\nexport interface LlmTokenBudgetSignal {\n signal: 'llm_token_budget_exceeded';\n /** Value of the configured key attribute (e.g. end-user id). */\n key: string;\n /** Tokens consumed inside the window. */\n tokens: number;\n budget: number;\n windowMs: number;\n}\n\nexport interface LlmActionChainSuspiciousSignal {\n signal: 'llm_action_chain_suspicious';\n /** Trace where the suspicious chain was observed. */\n traceId: string;\n /** Tool that followed untrusted-content processing. */\n toolName?: string;\n /** Milliseconds since the untrusted tool call on the same trace. */\n elapsedMs: number;\n untrustedTool?: string;\n}\n\nexport type SecuritySignal =\n | SuspiciousRequestSignal\n | AuthFailureBurstSignal\n | LlmExcessiveTokensSignal\n | LlmTokenBudgetSignal\n | LlmActionChainSuspiciousSignal;\n\nexport interface BurstOptions {\n /** HTTP statuses counted toward a burst. Default `[401, 403]`. */\n statuses?: number[];\n /** Denied responses within the window that trigger a signal. Default 10. */\n threshold?: number;\n /** Sliding window size in milliseconds. Default 60_000. */\n windowMs?: number;\n /**\n * Span attribute identifying the client. Default `client.address`\n * (falls back to `http.client_ip`).\n */\n keyAttribute?: string;\n /** Max distinct clients tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n}\n\nexport interface LlmSignalOptions {\n /**\n * Single-call token ceiling (`gen_ai.usage.total_tokens`, or input+output).\n * Default 100_000. Pass `false` to disable the per-call check.\n */\n maxTokensPerCall?: number | false;\n /**\n * Sliding-window token budget per key — catches slow-drip abuse that\n * stays under the per-call ceiling (OWASP LLM10: Unbounded Consumption).\n * Off unless configured.\n */\n tokenBudget?: {\n budget: number;\n /** Window size in milliseconds. Default 300_000 (5 min). */\n windowMs?: number;\n /**\n * Span attribute identifying the consumer. Default `enduser.id`\n * (falls back to `client.address`).\n */\n keyAttribute?: string;\n /** Max distinct keys tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n };\n}\n\nexport interface SecuritySignalProcessorOptions {\n /** Flag suspicious request paths on span start. Default true. */\n detectSuspiciousRequests?: boolean;\n /** Additional name → pattern pairs checked against the request target. */\n extraPatterns?: Record<string, RegExp>;\n /** Force-keep flagged spans through tail sampling. Default true. */\n forceKeepSuspicious?: boolean;\n /** HTTP statuses counted as denied. Default `[401, 403, 429]`. */\n deniedStatuses?: number[];\n /** Burst detection over denied responses. Pass `false` to disable. */\n burst?: BurstOptions | false;\n /**\n * LLM consumption signals from `gen_ai.*` spans (OWASP LLM10).\n * Enabled with the per-call ceiling by default; pass `false` to disable.\n */\n llm?: LlmSignalOptions | false;\n /**\n * Detect destructive MCP tool calls that follow untrusted-content tool usage\n * on the same trace (Google's \"read email then send externally\" pattern).\n * Default true.\n */\n detectSuspiciousActionChains?: boolean;\n /** Max ms between untrusted and destructive tool calls on one trace. Default 300_000. */\n actionChainWindowMs?: number;\n /** Emit `autotel.security.*` metrics. Default true. */\n metrics?: boolean;\n /** Called whenever a signal fires. Keep it fast and non-throwing. */\n onSignal?: (signal: SecuritySignal) => void;\n /** Clock override for tests. */\n now?: () => number;\n}\n\n/**\n * Conservative request-target patterns. Tuned for scanner/probe traffic —\n * high signal, low false-positive — not as a WAF. Extend via `extraPatterns`.\n */\nexport const SUSPICIOUS_REQUEST_PATTERNS: Record<string, RegExp> = {\n path_traversal: /(\\.\\.[/\\\\]|%2e%2e(%2f|%5c|\\/)|\\.\\.%2f|%252e%252e)/i,\n sensitive_file_probe:\n /(\\/\\.env\\b|\\/\\.git\\b|\\/etc\\/passwd|\\/wp-admin\\b|\\/\\.aws\\b|\\/id_rsa\\b)/i,\n sqli_probe:\n /(\\bunion\\b[\\s+%20]+(all[\\s+%20]+)?select\\b|'[\\s+%20]*or[\\s+%20]*'?1'?[\\s+%20]*=[\\s+%20]*'?1)/i,\n xss_probe: /(<script\\b|%3cscript)/i,\n null_byte: /%00/,\n};\n\nconst TARGET_ATTRIBUTES = [\n 'url.path',\n 'url.full',\n 'http.target',\n 'http.url',\n] as const;\n\nfunction readAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n keys: readonly string[],\n): AttributeValue | undefined {\n for (const key of keys) {\n const value = attributes[key];\n if (value !== undefined) return value;\n }\n return undefined;\n}\n\n/**\n * Weighted sliding-window counter with bounded key cardinality.\n * Weight 1 per hit counts occurrences; token counts as weights sum usage.\n */\nclass SlidingWindow {\n private readonly hits = new Map<string, Array<[number, number]>>();\n\n constructor(\n private readonly windowMs: number,\n private readonly maxKeys: number,\n ) {}\n\n /**\n * Record a hit; returns the totals inside the window before and after it,\n * so callers can signal exactly once on a threshold crossing.\n */\n record(key: string, now: number, weight = 1): { before: number; after: number } {\n let entries = this.hits.get(key);\n if (!entries) {\n // Bound memory: random client addresses must not grow the map forever.\n if (this.hits.size >= this.maxKeys) {\n const oldest = this.hits.keys().next().value;\n if (oldest !== undefined) this.hits.delete(oldest);\n }\n entries = [];\n this.hits.set(key, entries);\n }\n\n const cutoff = now - this.windowMs;\n while (entries.length > 0 && (entries[0] as [number, number])[0] < cutoff) {\n entries.shift();\n }\n\n let before = 0;\n for (const [, w] of entries) before += w;\n entries.push([now, weight]);\n return { before, after: before + weight };\n }\n}\n\n/** Burst detection options with defaults applied and the window attached. */\ninterface BurstConfig {\n statuses: Set<number>;\n threshold: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n}\n\nfunction resolveBurstConfig(\n option: BurstOptions | false | undefined,\n): BurstConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const windowMs = opts.windowMs ?? 60_000;\n return {\n statuses: new Set(opts.statuses ?? [401, 403]),\n threshold: opts.threshold ?? 10,\n windowMs,\n keyAttribute: opts.keyAttribute ?? 'client.address',\n window: new SlidingWindow(windowMs, opts.maxKeys ?? 10_000),\n };\n}\n\n/** LLM consumption options with defaults applied and windows attached. */\ninterface LlmConfig {\n maxTokensPerCall?: number;\n budget?: {\n budget: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n };\n}\n\nfunction resolveLlmConfig(\n option: LlmSignalOptions | false | undefined,\n): LlmConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const tokenBudget = opts.tokenBudget;\n const windowMs = tokenBudget?.windowMs ?? 300_000;\n return {\n maxTokensPerCall:\n opts.maxTokensPerCall === false\n ? undefined\n : (opts.maxTokensPerCall ?? 100_000),\n budget: tokenBudget && {\n budget: tokenBudget.budget,\n windowMs,\n keyAttribute: tokenBudget.keyAttribute ?? 'enduser.id',\n window: new SlidingWindow(windowMs, tokenBudget.maxKeys ?? 10_000),\n },\n };\n}\n\nconst MCP_TOOL_UNTRUSTED = 'mcp.tool.untrusted_content';\nconst MCP_TOOL_DESTRUCTIVE = 'mcp.tool.destructive';\nconst MCP_TOOL_NAME = 'mcp.tool.name';\n\ninterface UntrustedToolHit {\n toolName?: string;\n timestamp: number;\n}\n\nfunction pruneExpiredUntrustedTraces(\n hits: Map<string, UntrustedToolHit>,\n cutoff: number,\n): void {\n for (const [traceId, hit] of hits) {\n if (hit.timestamp < cutoff) {\n hits.delete(traceId);\n }\n }\n}\n\nfunction readTraceId(span: ReadableSpanLike): string | undefined {\n const fromContext = span.spanContext?.traceId;\n if (typeof fromContext === 'string' && fromContext.length > 0) {\n return fromContext;\n }\n const fromAttr = readAttribute(span.attributes, ['trace_id']);\n return typeof fromAttr === 'string' && fromAttr.length > 0 ? fromAttr : undefined;\n}\n\nfunction readBooleanAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n key: string,\n): boolean {\n return readAttribute(attributes, [key]) === true;\n}\n\nexport function createSecuritySignalProcessor(\n options: SecuritySignalProcessorOptions = {},\n): SecuritySignalProcessor {\n const detect = options.detectSuspiciousRequests !== false;\n const forceKeep = options.forceKeepSuspicious !== false;\n const metricsEnabled = options.metrics !== false;\n const deniedStatuses = new Set(\n options.deniedStatuses ?? SECURITY_DENIED_STATUSES,\n );\n const now = options.now ?? Date.now;\n\n const patterns: Record<string, RegExp> = {\n ...SUSPICIOUS_REQUEST_PATTERNS,\n ...options.extraPatterns,\n };\n\n const burst = resolveBurstConfig(options.burst);\n const llm = resolveLlmConfig(options.llm);\n const detectActionChains = options.detectSuspiciousActionChains !== false;\n const actionChainWindowMs = options.actionChainWindowMs ?? 300_000;\n const untrustedByTrace = new Map<string, UntrustedToolHit>();\n\n const counters = {\n suspicious: lazyCounter(\n SECURITY_METRICS.httpSuspicious,\n 'Requests matching suspicious-path patterns',\n ),\n denied: lazyCounter(\n SECURITY_METRICS.httpDenied,\n 'HTTP responses with denied status codes (401/403/429)',\n ),\n anomaly: lazyCounter(\n SECURITY_METRICS.anomaly,\n 'Security anomaly signals (e.g. auth-failure bursts)',\n ),\n };\n\n function count(\n which: keyof typeof counters,\n attributes: Record<string, string | number>,\n ): void {\n if (!metricsEnabled) return;\n counters[which].add(1, attributes);\n }\n\n function emit(signal: SecuritySignal): void {\n try {\n options.onSignal?.(signal);\n } catch {\n // Callbacks must never break the span pipeline.\n }\n }\n\n function checkDeniedResponse(span: ReadableSpanLike): void {\n const status = readAttribute(span.attributes, HTTP_STATUS_ATTRIBUTES);\n if (typeof status !== 'number' || !deniedStatuses.has(status)) return;\n\n count('denied', { status });\n\n if (!burst || !burst.statuses.has(status)) return;\n\n const key = readAttribute(span.attributes, [\n burst.keyAttribute,\n 'http.client_ip',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = burst.window.record(key, now());\n // Signal once per window on the exact crossing, not on every\n // subsequent hit — keeps anomaly volume bounded under attack.\n if (before < burst.threshold && after >= burst.threshold) {\n count('anomaly', { signal: 'auth_failure_burst', status });\n emit({\n signal: 'auth_failure_burst',\n key,\n count: after,\n windowMs: burst.windowMs,\n status,\n });\n }\n }\n\n function checkLlmConsumption(span: ReadableSpanLike): void {\n if (!llm) return;\n\n const total = readAttribute(span.attributes, ['gen_ai.usage.total_tokens']);\n let tokens: number | undefined;\n if (typeof total === 'number') {\n tokens = total;\n } else {\n const input = readAttribute(span.attributes, ['gen_ai.usage.input_tokens']);\n const output = readAttribute(span.attributes, [\n 'gen_ai.usage.output_tokens',\n ]);\n if (typeof input === 'number' || typeof output === 'number') {\n tokens =\n (typeof input === 'number' ? input : 0) +\n (typeof output === 'number' ? output : 0);\n }\n }\n if (tokens === undefined || tokens <= 0) return;\n\n if (llm.maxTokensPerCall !== undefined && tokens > llm.maxTokensPerCall) {\n const model = readAttribute(span.attributes, [\n 'gen_ai.response.model',\n 'gen_ai.request.model',\n ]);\n count('anomaly', { signal: 'llm_excessive_tokens' });\n emit({\n signal: 'llm_excessive_tokens',\n tokens,\n maxTokens: llm.maxTokensPerCall,\n ...(typeof model === 'string' && { model }),\n });\n }\n\n const budget = llm.budget;\n if (!budget) return;\n\n const key = readAttribute(span.attributes, [\n budget.keyAttribute,\n 'client.address',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = budget.window.record(key, now(), tokens);\n if (before < budget.budget && after >= budget.budget) {\n count('anomaly', { signal: 'llm_token_budget_exceeded' });\n emit({\n signal: 'llm_token_budget_exceeded',\n key,\n tokens: after,\n budget: budget.budget,\n windowMs: budget.windowMs,\n });\n }\n }\n\n function checkSuspiciousActionChain(span: MutableSpanLike): void {\n if (!detectActionChains) return;\n\n const traceId = readTraceId(span);\n if (!traceId) return;\n\n const nowMs = now();\n const cutoff = nowMs - actionChainWindowMs;\n pruneExpiredUntrustedTraces(untrustedByTrace, cutoff);\n\n if (readBooleanAttribute(span.attributes, MCP_TOOL_UNTRUSTED)) {\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n untrustedByTrace.set(traceId, {\n timestamp: nowMs,\n ...(typeof toolName === 'string' && { toolName }),\n });\n return;\n }\n\n if (!readBooleanAttribute(span.attributes, MCP_TOOL_DESTRUCTIVE)) {\n return;\n }\n\n const prior = untrustedByTrace.get(traceId);\n if (!prior) {\n return;\n }\n untrustedByTrace.delete(traceId);\n\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n const elapsedMs = nowMs - prior.timestamp;\n count('anomaly', { signal: 'llm_action_chain_suspicious' });\n emit({\n signal: 'llm_action_chain_suspicious',\n traceId,\n elapsedMs,\n ...(typeof toolName === 'string' && { toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n });\n applySecurityEventAttributes(\n span,\n {\n name: 'llm.action_chain.suspicious',\n category: 'llm',\n outcome: 'denied',\n severity: 'warning',\n reason: 'untrusted_then_destructive',\n targetType: 'trace',\n targetId: traceId,\n ...(typeof toolName === 'string' && { destructiveTool: toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n elapsedMs,\n },\n { forceKeep: true, metrics: false },\n );\n }\n\n return {\n onStart(span) {\n checkSuspiciousActionChain(span);\n if (!detect) return;\n\n const target = readAttribute(span.attributes, TARGET_ATTRIBUTES);\n if (typeof target !== 'string' || target.length === 0) return;\n\n for (const [name, pattern] of Object.entries(patterns)) {\n if (!pattern.test(target)) continue;\n\n span.setAttribute(SECURITY_ATTR.suspiciousRequest, true);\n span.setAttribute(SECURITY_ATTR.signal, name);\n if (forceKeep) {\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n }\n\n count('suspicious', { pattern: name });\n emit({ signal: 'suspicious_request', pattern: name, target });\n return; // first match wins — one signal per span\n }\n },\n\n onEnd(span) {\n checkDeniedResponse(span);\n checkLlmConsumption(span);\n },\n\n shutdown() {\n return Promise.resolve();\n },\n\n forceFlush() {\n return Promise.resolve();\n },\n };\n}\n","import { SECURITY_METRICS } from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\n\n/**\n * Security-telemetry heartbeat.\n *\n * A silently-dead telemetry pipeline is itself a security failure (NIST\n * SP 800-92: systems must not keep operating without visibility into\n * security events). `startSecurityHeartbeat()` emits the\n * `autotel.security.heartbeat` counter on a fixed interval so security\n * teams can alert on the ABSENCE of telemetry from a service:\n *\n * ```promql\n * absent(rate(autotel_security_heartbeat_total{service_name=\"api\"}[5m]))\n * ```\n *\n * ```typescript\n * const heartbeat = startSecurityHeartbeat();\n * // on shutdown:\n * heartbeat.stop();\n * ```\n */\n\nexport interface SecurityHeartbeatOptions {\n /** Beat interval in milliseconds. Default 60_000. */\n intervalMs?: number;\n /** Extra counter attributes (keep cardinality low — labels, not data). */\n attributes?: Record<string, string | number | boolean>;\n}\n\nexport interface SecurityHeartbeat {\n stop(): void;\n}\n\nexport function startSecurityHeartbeat(\n options: SecurityHeartbeatOptions = {},\n): SecurityHeartbeat {\n const intervalMs = options.intervalMs ?? 60_000;\n const attributes = options.attributes ?? {};\n\n const counter = lazyCounter(\n SECURITY_METRICS.heartbeat,\n 'Security-telemetry liveness signal — alert on its absence',\n );\n\n function beat(): void {\n counter.add(1, attributes);\n }\n\n beat(); // establish the series immediately, not one interval later\n const timer = setInterval(beat, intervalMs);\n // Never hold the process open just to beat.\n timer.unref?.();\n\n let stopped = false;\n return {\n stop() {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n },\n };\n}\n","import type { AuditContext } from './context.js';\nimport {\n securityEvent,\n type SecurityEventMetadata,\n type SecurityEventOptions,\n} from './security.js';\n\n/**\n * Metadata emitted when MCP protocol-boundary signals are bridged to the\n * unified `security.*` schema. Used by `autotel-mcp-instrumentation` when\n * `bridgeSecurityEvents` is enabled.\n */\nexport interface McpBridgedSecurityEvent {\n name: SecurityEventMetadata['name'];\n category: 'llm';\n outcome: SecurityEventMetadata['outcome'];\n severity?: SecurityEventMetadata['severity'];\n reason?: string;\n toolName?: string;\n verdict?: string;\n source?: string;\n [key: string]: unknown;\n}\n\nexport interface McpSecurityEventBridgeOptions extends SecurityEventOptions {\n /** Optional fixed audit context for bridged events. */\n ctx?: AuditContext;\n}\n\n/**\n * Create a bridge callback for MCP security observability → `securityEvent()`.\n *\n * @example\n * ```typescript\n * import { createMcpSecurityEventBridge } from 'autotel-audit';\n * import { instrumentMcpClient } from 'autotel-mcp-instrumentation/client';\n *\n * instrumentMcpClient(client, {\n * securityClassifier: heuristicInjectionClassifier(),\n * bridgeSecurityEvents: true,\n * securityEventBridge: createMcpSecurityEventBridge(),\n * });\n * ```\n */\nexport function createMcpSecurityEventBridge(\n options: McpSecurityEventBridgeOptions = {},\n): (metadata: McpBridgedSecurityEvent) => void {\n return (metadata) => {\n const { toolName, verdict, source, ...rest } = metadata;\n securityEvent(\n {\n ...rest,\n category: 'llm',\n ...(toolName !== undefined && { targetType: 'tool', targetId: toolName }),\n ...(verdict !== undefined && { verdict }),\n ...(source !== undefined && { source }),\n },\n options,\n );\n };\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n warnMissingLoggerOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\n\nexport type { AuditContext, OnMissingContext } from './context';\nexport * from './security';\nexport * from './security-signals';\nexport * from './security-heartbeat';\nexport * from './mcp-bridge';\n\nexport interface AuditMetadata {\n action: string;\n resource?: string;\n actorId?: string;\n category?: string;\n outcome?: 'success' | 'failure' | (string & {});\n [key: string]: unknown;\n}\n\nexport interface WithAuditOptions {\n ctx?: AuditContext;\n emitNow?: boolean;\n forceKeep?: boolean;\n logger?: RequestLogger;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: run un-audited, warn once). See {@link OnMissingContext}.\n */\n onMissingContext?: OnMissingContext;\n}\n\nfunction flattenAuditAttributes(\n metadata: AuditMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n 'autotel.audit': true,\n };\n\n for (const [key, value] of Object.entries(metadata)) {\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[`audit.${key}`] = attr;\n }\n }\n\n return attributes;\n}\n\nexport function forceKeepAuditEvent(ctx?: AuditContext): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute('autotel.audit.force_keep', true);\n}\n\nexport function setAuditAttributes(\n metadata: AuditMetadata,\n ctx?: AuditContext,\n): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttributes(flattenAuditAttributes(metadata));\n}\n\nexport async function withAudit<T>(\n metadata: AuditMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithAuditOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // No trace context: degrade per onMissingContext instead of throwing into\n // business logic. Audit is observability — it must never crash the caller.\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.action);\n }\n return fn(noopAuditContext(), options.logger ?? createNoopRequestLogger());\n }\n\n if (options.forceKeep !== false) {\n forceKeepAuditEvent(traceCtx);\n }\n\n setAuditAttributes(metadata, traceCtx);\n\n // A trace context may exist (e.g. caller-supplied options.ctx) without a\n // resolvable request logger. Record span attributes regardless and only skip\n // the canonical log line — never throw.\n let logger = options.logger ?? getRequestLoggerSafe() ?? undefined;\n if (!logger) {\n if ((options.onMissingContext ?? 'warn') === 'warn') {\n warnMissingLoggerOnce(metadata.action);\n }\n logger = createNoopRequestLogger();\n }\n logger.set({\n audit: {\n ...metadata,\n forceKeep: options.forceKeep !== false,\n },\n });\n\n try {\n const result = await fn(traceCtx, logger);\n\n if (!metadata.outcome) {\n setAuditAttributes({ ...metadata, outcome: 'success' }, traceCtx);\n }\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n setAuditAttributes({ ...metadata, outcome: 'failure' }, traceCtx);\n logger.error(asError, {\n audit: {\n action: metadata.action,\n resource: metadata.resource,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n throw asError;\n }\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -437,7 +437,8 @@ function createSecuritySignalProcessor(options = {}) {
437
437
  const traceId = readTraceId(span);
438
438
  if (!traceId) return;
439
439
  const nowMs = now();
440
- pruneExpiredUntrustedTraces(untrustedByTrace, nowMs - actionChainWindowMs);
440
+ const cutoff = nowMs - actionChainWindowMs;
441
+ pruneExpiredUntrustedTraces(untrustedByTrace, cutoff);
441
442
  if (readBooleanAttribute(span.attributes, MCP_TOOL_UNTRUSTED)) {
442
443
  const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);
443
444
  untrustedByTrace.set(traceId, {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/context.ts","../src/lazy-counter.ts","../src/security.ts","../src/security-signals.ts","../src/security-heartbeat.ts","../src/mcp-bridge.ts","../src/index.ts"],"sourcesContent":["import { getTraceContext, otelTrace } from 'autotel';\n\nexport interface AuditContext {\n traceId: string;\n spanId: string;\n correlationId: string;\n setAttribute(key: string, value: string | number | boolean): void;\n setAttributes(\n attrs: Record<string, string | number | boolean | string[] | number[] | boolean[]>,\n ): void;\n}\n\nconst MISSING_CONTEXT_MESSAGE =\n '[autotel-audit] No active trace context. Wrap the call in trace()/instrument(), pass options.ctx, ' +\n 'or set options.onMissingContext to \"warn\"/\"skip\" to degrade gracefully instead of throwing.';\n\n/**\n * Resolve an audit context without throwing. Returns `null` when no trace context\n * is available, so callers can degrade gracefully (best-effort instrumentation).\n */\nconst INVALID_TRACE_ID = '00000000000000000000000000000000';\n\nexport function resolveContextSafe(ctx?: AuditContext): AuditContext | null {\n if (ctx) return ctx;\n\n const span = otelTrace.getActiveSpan();\n if (!span) return null;\n\n // Resolve trace ids from autotel's context when available, otherwise from the\n // active OTel span itself, so audit works in any OTel setup — not only inside\n // autotel's own `trace()`.\n const ids = getTraceContext();\n const sc = span.spanContext();\n const traceId = ids?.traceId ?? sc.traceId;\n if (!traceId || traceId === INVALID_TRACE_ID) return null;\n\n return {\n traceId,\n spanId: ids?.spanId ?? sc.spanId,\n correlationId: ids?.correlationId ?? traceId.slice(0, 16),\n setAttribute: (key, value) => span.setAttribute(key, value),\n setAttributes: (attrs) => span.setAttributes(attrs),\n };\n}\n\nexport function resolveContext(ctx?: AuditContext): AuditContext {\n const resolved = resolveContextSafe(ctx);\n if (resolved) return resolved;\n throw new Error(MISSING_CONTEXT_MESSAGE);\n}\n\nexport { MISSING_CONTEXT_MESSAGE };\n\n/**\n * How instrumentation should behave when no trace context is available.\n *\n * - `throw` — fail fast (original behaviour). Use when telemetry is mandatory.\n * - `warn` — run the wrapped handler un-audited and log one warning per action (default).\n * - `skip` — run the wrapped handler un-audited, silently.\n *\n * Telemetry is observability: a missing context should never crash the business\n * logic it wraps, so the default is best-effort (`warn`).\n */\nexport type OnMissingContext = 'throw' | 'warn' | 'skip';\n\n/** A no-op {@link AuditContext} whose attribute setters do nothing. */\nexport function noopAuditContext(): AuditContext {\n return {\n traceId: '',\n spanId: '',\n correlationId: '',\n setAttribute() {},\n setAttributes() {},\n };\n}\n\nconst warnedMissingContext = new Set<string>();\nconst warnedMissingLogger = new Set<string>();\n\n/** Warn (once per action) that instrumentation is running without a trace context. */\nexport function warnMissingContextOnce(action: string): void {\n if (warnedMissingContext.has(action)) return;\n warnedMissingContext.add(action);\n console.warn(\n `[autotel-audit] No active trace context for \"${action}\" — running un-audited. ` +\n 'Wrap the call in trace()/instrument() or pass options.ctx to capture telemetry. ' +\n '(set options.onMissingContext: \"throw\" to fail fast, or \"skip\" to silence this warning)',\n );\n}\n\n/** Warn (once per action) that attributes were recorded but no canonical log line emitted. */\nexport function warnMissingLoggerOnce(action: string): void {\n if (warnedMissingLogger.has(action)) return;\n warnedMissingLogger.add(action);\n console.warn(\n `[autotel-audit] No request logger for \"${action}\" — attributes were recorded on the span, ` +\n 'but no canonical log line was emitted. Pass options.logger or run inside runWithRequestContext().',\n );\n}\n\nexport function toAttributeValue(\n value: unknown,\n): string | number | boolean | string[] | number[] | boolean[] | undefined {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n return value;\n }\n\n if (Array.isArray(value)) {\n if (value.every((entry) => typeof entry === 'string')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'number')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'boolean')) {\n return value;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (value === null || value === undefined) {\n return undefined;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n}\n","import { createCounter } from 'autotel';\n\nexport interface LazyCounter {\n add(value: number, attributes?: Record<string, string | number | boolean>): void;\n}\n\n/**\n * Counter that is created on first use (the meter may not be configured\n * until `init()` completes) and whose failures are swallowed — metrics\n * must never break event emission or the span pipeline.\n */\nexport function lazyCounter(name: string, description: string): LazyCounter {\n let counter: ReturnType<typeof createCounter> | undefined;\n return {\n add(value, attributes) {\n try {\n counter ??= createCounter(name, { description });\n counter.add(value, attributes);\n } catch {\n // Swallow — observability must never take the process down.\n }\n },\n };\n}\n","import { createHash } from 'node:crypto';\nimport {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n REDACTOR_PATTERNS,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n SECURITY_ATTR,\n SECURITY_METRICS,\n escalateSecuritySeverity,\n} from 'autotel/security-schema';\nimport type { SecuritySeverity } from 'autotel/security-schema';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\nimport { lazyCounter } from './lazy-counter';\n\nexport type { SecuritySeverity };\n\n/**\n * Security event categories, aligned with OWASP A09:2025\n * (Security Logging & Alerting Failures) and ASVS V7.\n */\nexport type SecurityEventCategory =\n | 'authentication'\n | 'authorization'\n | 'data_access'\n | 'admin_action'\n | 'configuration'\n | 'secrets'\n | 'rate_limit'\n | 'validation'\n | 'supply_chain'\n | 'llm';\n\nexport type SecurityOutcome =\n | 'success'\n | 'failure'\n | 'denied'\n | 'blocked'\n | 'error';\n\n/**\n * Well-known security event names. Free-form names are allowed —\n * this union exists for autocomplete and consistency across services.\n */\nexport type SuggestedSecurityEventName =\n | 'auth.login.success'\n | 'auth.login.failed'\n | 'auth.mfa.failed'\n | 'auth.session.revoked'\n | 'auth.password.reset'\n | 'auth.account.locked'\n | 'access.denied'\n | 'access.role.changed'\n | 'access.permission.changed'\n | 'access.tenant.violation'\n | 'admin.action'\n | 'config.changed'\n | 'secret.accessed'\n | 'secret.rotation.failed'\n | 'api_key.created'\n | 'api_key.revoked'\n | 'rate_limit.exceeded'\n | 'validation.failed'\n | 'webhook.signature.failed'\n | 'dependency.scan.failed'\n | 'llm.prompt_injection.detected'\n | 'llm.tool_call.denied'\n | 'llm.output.blocked'\n | 'llm.output.budget_exceeded'\n | 'llm.guard.triggered'\n | 'llm.action_chain.suspicious'\n | 'llm.manifest.suspicious'\n | 'llm.plan.risk.elevated';\n\nexport interface SecurityEventMetadata {\n /** Stable, dot-separated event name, e.g. `auth.login.failed`. */\n name: SuggestedSecurityEventName | (string & {});\n category: SecurityEventCategory;\n outcome: SecurityOutcome;\n /** Defaults to `info`. */\n severity?: SecuritySeverity;\n /** Stable identifier of the actor — an id or a `hashIdentifier()` digest, never raw PII. */\n actorId?: string;\n targetType?: string;\n targetId?: string;\n tenantId?: string;\n /** Short machine-readable reason, e.g. `invalid_password`. */\n reason?: string;\n [key: string]: unknown;\n}\n\nexport interface SecurityEventOptions {\n ctx?: AuditContext;\n /**\n * Security events are exempt from tail sampling by default —\n * an attack you sampled away is an attack you cannot investigate.\n * Pass `false` to opt out (e.g. very high-volume info events).\n */\n forceKeep?: boolean;\n emitNow?: boolean;\n logger?: RequestLogger;\n /**\n * Also increment the `autotel.security.events` counter\n * (attributes: event, category, outcome, severity) so security teams\n * can alert on rates without log-based alerting. Default true.\n *\n * Cardinality note: the event name is a counter attribute — keep names\n * to a stable catalogue, never interpolate user input into them.\n */\n metrics?: boolean;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: record nothing, warn once). A dropped security event is still\n * better than a crashed request — but the warning makes the gap visible.\n */\n onMissingContext?: OnMissingContext;\n}\n\nexport type WithSecurityOptions = SecurityEventOptions;\n\ninterface SecurityAttributeSink {\n setAttribute(\n key: string,\n value:\n | string\n | number\n | boolean\n | string[]\n | number[]\n | boolean[],\n ): unknown;\n}\n\n/**\n * Standard metadata fields and the schema attribute each maps to.\n * Drives both standard-field emission and the reserved-key check for the\n * custom-attribute loop — adding a field here is the whole change.\n */\nconst FIELD_ATTRIBUTES: Record<string, string> = {\n name: SECURITY_ATTR.event,\n category: SECURITY_ATTR.category,\n outcome: SECURITY_ATTR.outcome,\n severity: SECURITY_ATTR.severity,\n actorId: SECURITY_ATTR.actorId,\n targetType: SECURITY_ATTR.targetType,\n targetId: SECURITY_ATTR.targetId,\n tenantId: SECURITY_ATTR.tenantId,\n reason: SECURITY_ATTR.reason,\n};\n\nfunction flattenSecurityAttributes(\n metadata: SecurityEventMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n [SECURITY_ATTR.marker]: true,\n [SECURITY_ATTR.severity]: metadata.severity ?? 'info',\n };\n\n const droppedKeys: string[] = [];\n for (const [key, value] of Object.entries(metadata)) {\n const standardAttribute = FIELD_ATTRIBUTES[key];\n // Never emit values under credential-shaped custom keys, even by\n // accident. Reuses the core redactor's sensitive-key pattern so the\n // deny-list stays in one place.\n if (\n standardAttribute === undefined &&\n REDACTOR_PATTERNS.sensitiveKey.test(key)\n ) {\n droppedKeys.push(key);\n continue;\n }\n\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[standardAttribute ?? `security.${key}`] = attr;\n }\n }\n\n if (droppedKeys.length > 0) {\n attributes[SECURITY_ATTR.droppedKeys] = droppedKeys;\n }\n\n return attributes;\n}\n\nconst eventsCounter = lazyCounter(\n SECURITY_METRICS.events,\n 'Security events by name, category, outcome, and severity',\n);\n\nfunction countSecurityEvent(metadata: SecurityEventMetadata): void {\n eventsCounter.add(1, {\n event: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n });\n}\n\nexport function applySecurityEventAttributes(\n sink: SecurityAttributeSink,\n metadata: SecurityEventMetadata,\n options: Pick<SecurityEventOptions, 'forceKeep' | 'metrics'> = {},\n): void {\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (options.forceKeep !== false) {\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n sink.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n\n for (const [key, value] of Object.entries(flattenSecurityAttributes(metadata))) {\n sink.setAttribute(key, value);\n }\n}\n\n/**\n * Record a security event on the active trace and request logger.\n *\n * Events are force-kept through tail sampling by default and carry\n * `security.*` attributes (`security.event`, `security.category`,\n * `security.outcome`, `security.severity`) so backends can build\n * detection rules and dashboards from a stable schema.\n *\n * ```typescript\n * securityEvent({\n * name: 'auth.login.failed',\n * category: 'authentication',\n * outcome: 'failure',\n * severity: 'warning',\n * actorId: hashIdentifier(email),\n * reason: 'invalid_password',\n * });\n * ```\n */\nexport function securityEvent(\n metadata: SecurityEventMetadata,\n options: SecurityEventOptions = {},\n): void {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // Counters are independent of trace context — always record the security signal\n // even when there's no span to attach attributes to.\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.name);\n }\n return;\n }\n\n if (options.forceKeep !== false) {\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n traceCtx.setAttributes(flattenSecurityAttributes(metadata));\n\n const logger = options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n logger.set({\n security: {\n name: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n ...(metadata.reason !== undefined && { reason: metadata.reason }),\n forceKeep: options.forceKeep !== false,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n}\n\n/**\n * Wrap a security-sensitive operation. On success the event outcome is\n * recorded as given (default `success`); a thrown error records\n * `outcome: 'error'`, escalates the severity to at least `error`, and\n * rethrows.\n *\n * ```typescript\n * await withSecurity(\n * { name: 'api_key.created', category: 'secrets', outcome: 'success', actorId: userId },\n * async () => createApiKey(userId),\n * );\n * ```\n */\nexport async function withSecurity<T>(\n metadata: SecurityEventMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithSecurityOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n const logger =\n options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n const ctx = traceCtx ?? noopAuditContext();\n\n try {\n const result = await fn(ctx, logger);\n securityEvent(metadata, { ...options, ctx: traceCtx ?? undefined, logger });\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n securityEvent(\n {\n ...metadata,\n outcome: 'error',\n // A failed security-sensitive operation is never less than an error,\n // but an explicit `critical` stays critical.\n severity: escalateSecuritySeverity(metadata.severity ?? 'info', 'error'),\n },\n { ...options, ctx: traceCtx ?? undefined, logger },\n );\n logger.error(asError, {\n security: {\n name: metadata.name,\n category: metadata.category,\n },\n });\n throw asError;\n }\n}\n\nexport interface HashIdentifierOptions {\n /** Optional salt; use one stable per-deployment salt to defeat rainbow lookups. */\n salt?: string;\n /** Digest length in hex chars (default 16). */\n length?: number;\n}\n\n/**\n * Stable one-way digest for correlating PII-bearing identifiers\n * (emails, IPs) across events WITHOUT logging the raw value.\n *\n * NOT for secrets — never log secrets in any form, hashed or not.\n */\nexport function hashIdentifier(\n value: string,\n options: HashIdentifierOptions = {},\n): string {\n const length = options.length ?? 16;\n return createHash('sha256')\n .update(options.salt ? `${options.salt}:${value}` : value)\n .digest('hex')\n .slice(0, length);\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n} from 'autotel';\nimport {\n HTTP_STATUS_ATTRIBUTES,\n SECURITY_ATTR,\n SECURITY_DENIED_STATUSES,\n SECURITY_METRICS,\n} from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\nimport { applySecurityEventAttributes } from './security.js';\n\n/**\n * Zero-code security signal derivation from spans you already have.\n *\n * `createSecuritySignalProcessor()` watches ordinary HTTP server spans and:\n *\n * - flags suspicious request paths (traversal, `.env`/`.git` probes,\n * SQLi/XSS probes) at span start, marking them `security.suspicious_request`\n * and force-keeping them through tail sampling\n * - counts denied responses (401/403/429 by default) into the\n * `autotel.security.http.denied` metric\n * - detects auth-failure bursts per client (sliding window) and surfaces\n * them via the `autotel.security.anomaly` metric and an `onSignal` callback\n *\n * ```typescript\n * init({\n * service: 'api',\n * spanProcessors: [createSecuritySignalProcessor()],\n * });\n * ```\n *\n * Detection rules, alert thresholds, and dashboards belong in your\n * observability backend — this processor's job is to make the signals\n * exist, survive sampling, and stay queryable under a stable schema.\n */\n\n// Structural subset of @opentelemetry/sdk-trace-base types — kept local so\n// autotel-audit adds no new dependencies. Objects returned here satisfy the\n// real SpanProcessor interface structurally (must mirror @opentelemetry/api's\n// AttributeValue, including nullable array entries).\ntype AttributeValue =\n | string\n | number\n | boolean\n | Array<null | undefined | string>\n | Array<null | undefined | number>\n | Array<null | undefined | boolean>;\n\ninterface MutableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n setAttribute(key: string, value: AttributeValue): unknown;\n}\n\ninterface ReadableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n}\n\nexport interface SecuritySignalProcessor {\n onStart(span: MutableSpanLike, parentContext?: unknown): void;\n onEnd(span: ReadableSpanLike): void;\n shutdown(): Promise<void>;\n forceFlush(): Promise<void>;\n}\n\nexport interface SuspiciousRequestSignal {\n signal: 'suspicious_request';\n /** Which pattern matched, e.g. `path_traversal`. */\n pattern: string;\n /** The matched request path/URL (as found on the span). */\n target: string;\n}\n\nexport interface AuthFailureBurstSignal {\n signal: 'auth_failure_burst';\n /** Value of the configured key attribute (e.g. client address). */\n key: string;\n /** Denied responses observed inside the window. */\n count: number;\n windowMs: number;\n status: number;\n}\n\nexport interface LlmExcessiveTokensSignal {\n signal: 'llm_excessive_tokens';\n /** Total tokens consumed by the single LLM call. */\n tokens: number;\n maxTokens: number;\n model?: string;\n}\n\nexport interface LlmTokenBudgetSignal {\n signal: 'llm_token_budget_exceeded';\n /** Value of the configured key attribute (e.g. end-user id). */\n key: string;\n /** Tokens consumed inside the window. */\n tokens: number;\n budget: number;\n windowMs: number;\n}\n\nexport interface LlmActionChainSuspiciousSignal {\n signal: 'llm_action_chain_suspicious';\n /** Trace where the suspicious chain was observed. */\n traceId: string;\n /** Tool that followed untrusted-content processing. */\n toolName?: string;\n /** Milliseconds since the untrusted tool call on the same trace. */\n elapsedMs: number;\n untrustedTool?: string;\n}\n\nexport type SecuritySignal =\n | SuspiciousRequestSignal\n | AuthFailureBurstSignal\n | LlmExcessiveTokensSignal\n | LlmTokenBudgetSignal\n | LlmActionChainSuspiciousSignal;\n\nexport interface BurstOptions {\n /** HTTP statuses counted toward a burst. Default `[401, 403]`. */\n statuses?: number[];\n /** Denied responses within the window that trigger a signal. Default 10. */\n threshold?: number;\n /** Sliding window size in milliseconds. Default 60_000. */\n windowMs?: number;\n /**\n * Span attribute identifying the client. Default `client.address`\n * (falls back to `http.client_ip`).\n */\n keyAttribute?: string;\n /** Max distinct clients tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n}\n\nexport interface LlmSignalOptions {\n /**\n * Single-call token ceiling (`gen_ai.usage.total_tokens`, or input+output).\n * Default 100_000. Pass `false` to disable the per-call check.\n */\n maxTokensPerCall?: number | false;\n /**\n * Sliding-window token budget per key — catches slow-drip abuse that\n * stays under the per-call ceiling (OWASP LLM10: Unbounded Consumption).\n * Off unless configured.\n */\n tokenBudget?: {\n budget: number;\n /** Window size in milliseconds. Default 300_000 (5 min). */\n windowMs?: number;\n /**\n * Span attribute identifying the consumer. Default `enduser.id`\n * (falls back to `client.address`).\n */\n keyAttribute?: string;\n /** Max distinct keys tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n };\n}\n\nexport interface SecuritySignalProcessorOptions {\n /** Flag suspicious request paths on span start. Default true. */\n detectSuspiciousRequests?: boolean;\n /** Additional name → pattern pairs checked against the request target. */\n extraPatterns?: Record<string, RegExp>;\n /** Force-keep flagged spans through tail sampling. Default true. */\n forceKeepSuspicious?: boolean;\n /** HTTP statuses counted as denied. Default `[401, 403, 429]`. */\n deniedStatuses?: number[];\n /** Burst detection over denied responses. Pass `false` to disable. */\n burst?: BurstOptions | false;\n /**\n * LLM consumption signals from `gen_ai.*` spans (OWASP LLM10).\n * Enabled with the per-call ceiling by default; pass `false` to disable.\n */\n llm?: LlmSignalOptions | false;\n /**\n * Detect destructive MCP tool calls that follow untrusted-content tool usage\n * on the same trace (Google's \"read email then send externally\" pattern).\n * Default true.\n */\n detectSuspiciousActionChains?: boolean;\n /** Max ms between untrusted and destructive tool calls on one trace. Default 300_000. */\n actionChainWindowMs?: number;\n /** Emit `autotel.security.*` metrics. Default true. */\n metrics?: boolean;\n /** Called whenever a signal fires. Keep it fast and non-throwing. */\n onSignal?: (signal: SecuritySignal) => void;\n /** Clock override for tests. */\n now?: () => number;\n}\n\n/**\n * Conservative request-target patterns. Tuned for scanner/probe traffic —\n * high signal, low false-positive — not as a WAF. Extend via `extraPatterns`.\n */\nexport const SUSPICIOUS_REQUEST_PATTERNS: Record<string, RegExp> = {\n path_traversal: /(\\.\\.[/\\\\]|%2e%2e(%2f|%5c|\\/)|\\.\\.%2f|%252e%252e)/i,\n sensitive_file_probe:\n /(\\/\\.env\\b|\\/\\.git\\b|\\/etc\\/passwd|\\/wp-admin\\b|\\/\\.aws\\b|\\/id_rsa\\b)/i,\n sqli_probe:\n /(\\bunion\\b[\\s+%20]+(all[\\s+%20]+)?select\\b|'[\\s+%20]*or[\\s+%20]*'?1'?[\\s+%20]*=[\\s+%20]*'?1)/i,\n xss_probe: /(<script\\b|%3cscript)/i,\n null_byte: /%00/,\n};\n\nconst TARGET_ATTRIBUTES = [\n 'url.path',\n 'url.full',\n 'http.target',\n 'http.url',\n] as const;\n\nfunction readAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n keys: readonly string[],\n): AttributeValue | undefined {\n for (const key of keys) {\n const value = attributes[key];\n if (value !== undefined) return value;\n }\n return undefined;\n}\n\n/**\n * Weighted sliding-window counter with bounded key cardinality.\n * Weight 1 per hit counts occurrences; token counts as weights sum usage.\n */\nclass SlidingWindow {\n private readonly hits = new Map<string, Array<[number, number]>>();\n\n constructor(\n private readonly windowMs: number,\n private readonly maxKeys: number,\n ) {}\n\n /**\n * Record a hit; returns the totals inside the window before and after it,\n * so callers can signal exactly once on a threshold crossing.\n */\n record(key: string, now: number, weight = 1): { before: number; after: number } {\n let entries = this.hits.get(key);\n if (!entries) {\n // Bound memory: random client addresses must not grow the map forever.\n if (this.hits.size >= this.maxKeys) {\n const oldest = this.hits.keys().next().value;\n if (oldest !== undefined) this.hits.delete(oldest);\n }\n entries = [];\n this.hits.set(key, entries);\n }\n\n const cutoff = now - this.windowMs;\n while (entries.length > 0 && (entries[0] as [number, number])[0] < cutoff) {\n entries.shift();\n }\n\n let before = 0;\n for (const [, w] of entries) before += w;\n entries.push([now, weight]);\n return { before, after: before + weight };\n }\n}\n\n/** Burst detection options with defaults applied and the window attached. */\ninterface BurstConfig {\n statuses: Set<number>;\n threshold: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n}\n\nfunction resolveBurstConfig(\n option: BurstOptions | false | undefined,\n): BurstConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const windowMs = opts.windowMs ?? 60_000;\n return {\n statuses: new Set(opts.statuses ?? [401, 403]),\n threshold: opts.threshold ?? 10,\n windowMs,\n keyAttribute: opts.keyAttribute ?? 'client.address',\n window: new SlidingWindow(windowMs, opts.maxKeys ?? 10_000),\n };\n}\n\n/** LLM consumption options with defaults applied and windows attached. */\ninterface LlmConfig {\n maxTokensPerCall?: number;\n budget?: {\n budget: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n };\n}\n\nfunction resolveLlmConfig(\n option: LlmSignalOptions | false | undefined,\n): LlmConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const tokenBudget = opts.tokenBudget;\n const windowMs = tokenBudget?.windowMs ?? 300_000;\n return {\n maxTokensPerCall:\n opts.maxTokensPerCall === false\n ? undefined\n : (opts.maxTokensPerCall ?? 100_000),\n budget: tokenBudget && {\n budget: tokenBudget.budget,\n windowMs,\n keyAttribute: tokenBudget.keyAttribute ?? 'enduser.id',\n window: new SlidingWindow(windowMs, tokenBudget.maxKeys ?? 10_000),\n },\n };\n}\n\nconst MCP_TOOL_UNTRUSTED = 'mcp.tool.untrusted_content';\nconst MCP_TOOL_DESTRUCTIVE = 'mcp.tool.destructive';\nconst MCP_TOOL_NAME = 'mcp.tool.name';\n\ninterface UntrustedToolHit {\n toolName?: string;\n timestamp: number;\n}\n\nfunction pruneExpiredUntrustedTraces(\n hits: Map<string, UntrustedToolHit>,\n cutoff: number,\n): void {\n for (const [traceId, hit] of hits) {\n if (hit.timestamp < cutoff) {\n hits.delete(traceId);\n }\n }\n}\n\nfunction readTraceId(span: ReadableSpanLike): string | undefined {\n const fromContext = span.spanContext?.traceId;\n if (typeof fromContext === 'string' && fromContext.length > 0) {\n return fromContext;\n }\n const fromAttr = readAttribute(span.attributes, ['trace_id']);\n return typeof fromAttr === 'string' && fromAttr.length > 0 ? fromAttr : undefined;\n}\n\nfunction readBooleanAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n key: string,\n): boolean {\n return readAttribute(attributes, [key]) === true;\n}\n\nexport function createSecuritySignalProcessor(\n options: SecuritySignalProcessorOptions = {},\n): SecuritySignalProcessor {\n const detect = options.detectSuspiciousRequests !== false;\n const forceKeep = options.forceKeepSuspicious !== false;\n const metricsEnabled = options.metrics !== false;\n const deniedStatuses = new Set(\n options.deniedStatuses ?? SECURITY_DENIED_STATUSES,\n );\n const now = options.now ?? Date.now;\n\n const patterns: Record<string, RegExp> = {\n ...SUSPICIOUS_REQUEST_PATTERNS,\n ...options.extraPatterns,\n };\n\n const burst = resolveBurstConfig(options.burst);\n const llm = resolveLlmConfig(options.llm);\n const detectActionChains = options.detectSuspiciousActionChains !== false;\n const actionChainWindowMs = options.actionChainWindowMs ?? 300_000;\n const untrustedByTrace = new Map<string, UntrustedToolHit>();\n\n const counters = {\n suspicious: lazyCounter(\n SECURITY_METRICS.httpSuspicious,\n 'Requests matching suspicious-path patterns',\n ),\n denied: lazyCounter(\n SECURITY_METRICS.httpDenied,\n 'HTTP responses with denied status codes (401/403/429)',\n ),\n anomaly: lazyCounter(\n SECURITY_METRICS.anomaly,\n 'Security anomaly signals (e.g. auth-failure bursts)',\n ),\n };\n\n function count(\n which: keyof typeof counters,\n attributes: Record<string, string | number>,\n ): void {\n if (!metricsEnabled) return;\n counters[which].add(1, attributes);\n }\n\n function emit(signal: SecuritySignal): void {\n try {\n options.onSignal?.(signal);\n } catch {\n // Callbacks must never break the span pipeline.\n }\n }\n\n function checkDeniedResponse(span: ReadableSpanLike): void {\n const status = readAttribute(span.attributes, HTTP_STATUS_ATTRIBUTES);\n if (typeof status !== 'number' || !deniedStatuses.has(status)) return;\n\n count('denied', { status });\n\n if (!burst || !burst.statuses.has(status)) return;\n\n const key = readAttribute(span.attributes, [\n burst.keyAttribute,\n 'http.client_ip',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = burst.window.record(key, now());\n // Signal once per window on the exact crossing, not on every\n // subsequent hit — keeps anomaly volume bounded under attack.\n if (before < burst.threshold && after >= burst.threshold) {\n count('anomaly', { signal: 'auth_failure_burst', status });\n emit({\n signal: 'auth_failure_burst',\n key,\n count: after,\n windowMs: burst.windowMs,\n status,\n });\n }\n }\n\n function checkLlmConsumption(span: ReadableSpanLike): void {\n if (!llm) return;\n\n const total = readAttribute(span.attributes, ['gen_ai.usage.total_tokens']);\n let tokens: number | undefined;\n if (typeof total === 'number') {\n tokens = total;\n } else {\n const input = readAttribute(span.attributes, ['gen_ai.usage.input_tokens']);\n const output = readAttribute(span.attributes, [\n 'gen_ai.usage.output_tokens',\n ]);\n if (typeof input === 'number' || typeof output === 'number') {\n tokens =\n (typeof input === 'number' ? input : 0) +\n (typeof output === 'number' ? output : 0);\n }\n }\n if (tokens === undefined || tokens <= 0) return;\n\n if (llm.maxTokensPerCall !== undefined && tokens > llm.maxTokensPerCall) {\n const model = readAttribute(span.attributes, [\n 'gen_ai.response.model',\n 'gen_ai.request.model',\n ]);\n count('anomaly', { signal: 'llm_excessive_tokens' });\n emit({\n signal: 'llm_excessive_tokens',\n tokens,\n maxTokens: llm.maxTokensPerCall,\n ...(typeof model === 'string' && { model }),\n });\n }\n\n const budget = llm.budget;\n if (!budget) return;\n\n const key = readAttribute(span.attributes, [\n budget.keyAttribute,\n 'client.address',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = budget.window.record(key, now(), tokens);\n if (before < budget.budget && after >= budget.budget) {\n count('anomaly', { signal: 'llm_token_budget_exceeded' });\n emit({\n signal: 'llm_token_budget_exceeded',\n key,\n tokens: after,\n budget: budget.budget,\n windowMs: budget.windowMs,\n });\n }\n }\n\n function checkSuspiciousActionChain(span: MutableSpanLike): void {\n if (!detectActionChains) return;\n\n const traceId = readTraceId(span);\n if (!traceId) return;\n\n const nowMs = now();\n const cutoff = nowMs - actionChainWindowMs;\n pruneExpiredUntrustedTraces(untrustedByTrace, cutoff);\n\n if (readBooleanAttribute(span.attributes, MCP_TOOL_UNTRUSTED)) {\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n untrustedByTrace.set(traceId, {\n timestamp: nowMs,\n ...(typeof toolName === 'string' && { toolName }),\n });\n return;\n }\n\n if (!readBooleanAttribute(span.attributes, MCP_TOOL_DESTRUCTIVE)) {\n return;\n }\n\n const prior = untrustedByTrace.get(traceId);\n if (!prior) {\n return;\n }\n untrustedByTrace.delete(traceId);\n\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n const elapsedMs = nowMs - prior.timestamp;\n count('anomaly', { signal: 'llm_action_chain_suspicious' });\n emit({\n signal: 'llm_action_chain_suspicious',\n traceId,\n elapsedMs,\n ...(typeof toolName === 'string' && { toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n });\n applySecurityEventAttributes(\n span,\n {\n name: 'llm.action_chain.suspicious',\n category: 'llm',\n outcome: 'denied',\n severity: 'warning',\n reason: 'untrusted_then_destructive',\n targetType: 'trace',\n targetId: traceId,\n ...(typeof toolName === 'string' && { destructiveTool: toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n elapsedMs,\n },\n { forceKeep: true, metrics: false },\n );\n }\n\n return {\n onStart(span) {\n checkSuspiciousActionChain(span);\n if (!detect) return;\n\n const target = readAttribute(span.attributes, TARGET_ATTRIBUTES);\n if (typeof target !== 'string' || target.length === 0) return;\n\n for (const [name, pattern] of Object.entries(patterns)) {\n if (!pattern.test(target)) continue;\n\n span.setAttribute(SECURITY_ATTR.suspiciousRequest, true);\n span.setAttribute(SECURITY_ATTR.signal, name);\n if (forceKeep) {\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n }\n\n count('suspicious', { pattern: name });\n emit({ signal: 'suspicious_request', pattern: name, target });\n return; // first match wins — one signal per span\n }\n },\n\n onEnd(span) {\n checkDeniedResponse(span);\n checkLlmConsumption(span);\n },\n\n shutdown() {\n return Promise.resolve();\n },\n\n forceFlush() {\n return Promise.resolve();\n },\n };\n}\n","import { SECURITY_METRICS } from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\n\n/**\n * Security-telemetry heartbeat.\n *\n * A silently-dead telemetry pipeline is itself a security failure (NIST\n * SP 800-92: systems must not keep operating without visibility into\n * security events). `startSecurityHeartbeat()` emits the\n * `autotel.security.heartbeat` counter on a fixed interval so security\n * teams can alert on the ABSENCE of telemetry from a service:\n *\n * ```promql\n * absent(rate(autotel_security_heartbeat_total{service_name=\"api\"}[5m]))\n * ```\n *\n * ```typescript\n * const heartbeat = startSecurityHeartbeat();\n * // on shutdown:\n * heartbeat.stop();\n * ```\n */\n\nexport interface SecurityHeartbeatOptions {\n /** Beat interval in milliseconds. Default 60_000. */\n intervalMs?: number;\n /** Extra counter attributes (keep cardinality low — labels, not data). */\n attributes?: Record<string, string | number | boolean>;\n}\n\nexport interface SecurityHeartbeat {\n stop(): void;\n}\n\nexport function startSecurityHeartbeat(\n options: SecurityHeartbeatOptions = {},\n): SecurityHeartbeat {\n const intervalMs = options.intervalMs ?? 60_000;\n const attributes = options.attributes ?? {};\n\n const counter = lazyCounter(\n SECURITY_METRICS.heartbeat,\n 'Security-telemetry liveness signal — alert on its absence',\n );\n\n function beat(): void {\n counter.add(1, attributes);\n }\n\n beat(); // establish the series immediately, not one interval later\n const timer = setInterval(beat, intervalMs);\n // Never hold the process open just to beat.\n timer.unref?.();\n\n let stopped = false;\n return {\n stop() {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n },\n };\n}\n","import type { AuditContext } from './context.js';\nimport {\n securityEvent,\n type SecurityEventMetadata,\n type SecurityEventOptions,\n} from './security.js';\n\n/**\n * Metadata emitted when MCP protocol-boundary signals are bridged to the\n * unified `security.*` schema. Used by `autotel-mcp-instrumentation` when\n * `bridgeSecurityEvents` is enabled.\n */\nexport interface McpBridgedSecurityEvent {\n name: SecurityEventMetadata['name'];\n category: 'llm';\n outcome: SecurityEventMetadata['outcome'];\n severity?: SecurityEventMetadata['severity'];\n reason?: string;\n toolName?: string;\n verdict?: string;\n source?: string;\n [key: string]: unknown;\n}\n\nexport interface McpSecurityEventBridgeOptions extends SecurityEventOptions {\n /** Optional fixed audit context for bridged events. */\n ctx?: AuditContext;\n}\n\n/**\n * Create a bridge callback for MCP security observability → `securityEvent()`.\n *\n * @example\n * ```typescript\n * import { createMcpSecurityEventBridge } from 'autotel-audit';\n * import { instrumentMcpClient } from 'autotel-mcp-instrumentation/client';\n *\n * instrumentMcpClient(client, {\n * securityClassifier: heuristicInjectionClassifier(),\n * bridgeSecurityEvents: true,\n * securityEventBridge: createMcpSecurityEventBridge(),\n * });\n * ```\n */\nexport function createMcpSecurityEventBridge(\n options: McpSecurityEventBridgeOptions = {},\n): (metadata: McpBridgedSecurityEvent) => void {\n return (metadata) => {\n const { toolName, verdict, source, ...rest } = metadata;\n securityEvent(\n {\n ...rest,\n category: 'llm',\n ...(toolName !== undefined && { targetType: 'tool', targetId: toolName }),\n ...(verdict !== undefined && { verdict }),\n ...(source !== undefined && { source }),\n },\n options,\n );\n };\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n warnMissingLoggerOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\n\nexport type { AuditContext, OnMissingContext } from './context';\nexport * from './security';\nexport * from './security-signals';\nexport * from './security-heartbeat';\nexport * from './mcp-bridge';\n\nexport interface AuditMetadata {\n action: string;\n resource?: string;\n actorId?: string;\n category?: string;\n outcome?: 'success' | 'failure' | (string & {});\n [key: string]: unknown;\n}\n\nexport interface WithAuditOptions {\n ctx?: AuditContext;\n emitNow?: boolean;\n forceKeep?: boolean;\n logger?: RequestLogger;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: run un-audited, warn once). See {@link OnMissingContext}.\n */\n onMissingContext?: OnMissingContext;\n}\n\nfunction flattenAuditAttributes(\n metadata: AuditMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n 'autotel.audit': true,\n };\n\n for (const [key, value] of Object.entries(metadata)) {\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[`audit.${key}`] = attr;\n }\n }\n\n return attributes;\n}\n\nexport function forceKeepAuditEvent(ctx?: AuditContext): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute('autotel.audit.force_keep', true);\n}\n\nexport function setAuditAttributes(\n metadata: AuditMetadata,\n ctx?: AuditContext,\n): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttributes(flattenAuditAttributes(metadata));\n}\n\nexport async function withAudit<T>(\n metadata: AuditMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithAuditOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // No trace context: degrade per onMissingContext instead of throwing into\n // business logic. Audit is observability — it must never crash the caller.\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.action);\n }\n return fn(noopAuditContext(), options.logger ?? createNoopRequestLogger());\n }\n\n if (options.forceKeep !== false) {\n forceKeepAuditEvent(traceCtx);\n }\n\n setAuditAttributes(metadata, traceCtx);\n\n // A trace context may exist (e.g. caller-supplied options.ctx) without a\n // resolvable request logger. Record span attributes regardless and only skip\n // the canonical log line — never throw.\n let logger = options.logger ?? getRequestLoggerSafe() ?? undefined;\n if (!logger) {\n if ((options.onMissingContext ?? 'warn') === 'warn') {\n warnMissingLoggerOnce(metadata.action);\n }\n logger = createNoopRequestLogger();\n }\n logger.set({\n audit: {\n ...metadata,\n forceKeep: options.forceKeep !== false,\n },\n });\n\n try {\n const result = await fn(traceCtx, logger);\n\n if (!metadata.outcome) {\n setAuditAttributes({ ...metadata, outcome: 'success' }, traceCtx);\n }\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n setAuditAttributes({ ...metadata, outcome: 'failure' }, traceCtx);\n logger.error(asError, {\n audit: {\n action: metadata.action,\n resource: metadata.resource,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n throw asError;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/context.ts","../src/lazy-counter.ts","../src/security.ts","../src/security-signals.ts","../src/security-heartbeat.ts","../src/mcp-bridge.ts","../src/index.ts"],"sourcesContent":["import { getTraceContext, otelTrace } from 'autotel';\n\nexport interface AuditContext {\n traceId: string;\n spanId: string;\n correlationId: string;\n setAttribute(key: string, value: string | number | boolean): void;\n setAttributes(\n attrs: Record<string, string | number | boolean | string[] | number[] | boolean[]>,\n ): void;\n}\n\nconst MISSING_CONTEXT_MESSAGE =\n '[autotel-audit] No active trace context. Wrap the call in trace()/instrument(), pass options.ctx, ' +\n 'or set options.onMissingContext to \"warn\"/\"skip\" to degrade gracefully instead of throwing.';\n\n/**\n * Resolve an audit context without throwing. Returns `null` when no trace context\n * is available, so callers can degrade gracefully (best-effort instrumentation).\n */\nconst INVALID_TRACE_ID = '00000000000000000000000000000000';\n\nexport function resolveContextSafe(ctx?: AuditContext): AuditContext | null {\n if (ctx) return ctx;\n\n const span = otelTrace.getActiveSpan();\n if (!span) return null;\n\n // Resolve trace ids from autotel's context when available, otherwise from the\n // active OTel span itself, so audit works in any OTel setup — not only inside\n // autotel's own `trace()`.\n const ids = getTraceContext();\n const sc = span.spanContext();\n const traceId = ids?.traceId ?? sc.traceId;\n if (!traceId || traceId === INVALID_TRACE_ID) return null;\n\n return {\n traceId,\n spanId: ids?.spanId ?? sc.spanId,\n correlationId: ids?.correlationId ?? traceId.slice(0, 16),\n setAttribute: (key, value) => span.setAttribute(key, value),\n setAttributes: (attrs) => span.setAttributes(attrs),\n };\n}\n\nexport function resolveContext(ctx?: AuditContext): AuditContext {\n const resolved = resolveContextSafe(ctx);\n if (resolved) return resolved;\n throw new Error(MISSING_CONTEXT_MESSAGE);\n}\n\nexport { MISSING_CONTEXT_MESSAGE };\n\n/**\n * How instrumentation should behave when no trace context is available.\n *\n * - `throw` — fail fast (original behaviour). Use when telemetry is mandatory.\n * - `warn` — run the wrapped handler un-audited and log one warning per action (default).\n * - `skip` — run the wrapped handler un-audited, silently.\n *\n * Telemetry is observability: a missing context should never crash the business\n * logic it wraps, so the default is best-effort (`warn`).\n */\nexport type OnMissingContext = 'throw' | 'warn' | 'skip';\n\n/** A no-op {@link AuditContext} whose attribute setters do nothing. */\nexport function noopAuditContext(): AuditContext {\n return {\n traceId: '',\n spanId: '',\n correlationId: '',\n setAttribute() {},\n setAttributes() {},\n };\n}\n\nconst warnedMissingContext = new Set<string>();\nconst warnedMissingLogger = new Set<string>();\n\n/** Warn (once per action) that instrumentation is running without a trace context. */\nexport function warnMissingContextOnce(action: string): void {\n if (warnedMissingContext.has(action)) return;\n warnedMissingContext.add(action);\n console.warn(\n `[autotel-audit] No active trace context for \"${action}\" — running un-audited. ` +\n 'Wrap the call in trace()/instrument() or pass options.ctx to capture telemetry. ' +\n '(set options.onMissingContext: \"throw\" to fail fast, or \"skip\" to silence this warning)',\n );\n}\n\n/** Warn (once per action) that attributes were recorded but no canonical log line emitted. */\nexport function warnMissingLoggerOnce(action: string): void {\n if (warnedMissingLogger.has(action)) return;\n warnedMissingLogger.add(action);\n console.warn(\n `[autotel-audit] No request logger for \"${action}\" — attributes were recorded on the span, ` +\n 'but no canonical log line was emitted. Pass options.logger or run inside runWithRequestContext().',\n );\n}\n\nexport function toAttributeValue(\n value: unknown,\n): string | number | boolean | string[] | number[] | boolean[] | undefined {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n return value;\n }\n\n if (Array.isArray(value)) {\n if (value.every((entry) => typeof entry === 'string')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'number')) {\n return value;\n }\n\n if (value.every((entry) => typeof entry === 'boolean')) {\n return value;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (value === null || value === undefined) {\n return undefined;\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return '<serialization-failed>';\n }\n}\n","import { createCounter } from 'autotel';\n\nexport interface LazyCounter {\n add(value: number, attributes?: Record<string, string | number | boolean>): void;\n}\n\n/**\n * Counter that is created on first use (the meter may not be configured\n * until `init()` completes) and whose failures are swallowed — metrics\n * must never break event emission or the span pipeline.\n */\nexport function lazyCounter(name: string, description: string): LazyCounter {\n let counter: ReturnType<typeof createCounter> | undefined;\n return {\n add(value, attributes) {\n try {\n counter ??= createCounter(name, { description });\n counter.add(value, attributes);\n } catch {\n // Swallow — observability must never take the process down.\n }\n },\n };\n}\n","import { createHash } from 'node:crypto';\nimport {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n REDACTOR_PATTERNS,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n SECURITY_ATTR,\n SECURITY_METRICS,\n escalateSecuritySeverity,\n} from 'autotel/security-schema';\nimport type { SecuritySeverity } from 'autotel/security-schema';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\nimport { lazyCounter } from './lazy-counter';\n\nexport type { SecuritySeverity };\n\n/**\n * Security event categories, aligned with OWASP A09:2025\n * (Security Logging & Alerting Failures) and ASVS V7.\n */\nexport type SecurityEventCategory =\n | 'authentication'\n | 'authorization'\n | 'data_access'\n | 'admin_action'\n | 'configuration'\n | 'secrets'\n | 'rate_limit'\n | 'validation'\n | 'supply_chain'\n | 'llm';\n\nexport type SecurityOutcome =\n | 'success'\n | 'failure'\n | 'denied'\n | 'blocked'\n | 'error';\n\n/**\n * Well-known security event names. Free-form names are allowed —\n * this union exists for autocomplete and consistency across services.\n */\nexport type SuggestedSecurityEventName =\n | 'auth.login.success'\n | 'auth.login.failed'\n | 'auth.mfa.failed'\n | 'auth.session.revoked'\n | 'auth.password.reset'\n | 'auth.account.locked'\n | 'access.denied'\n | 'access.role.changed'\n | 'access.permission.changed'\n | 'access.tenant.violation'\n | 'admin.action'\n | 'config.changed'\n | 'secret.accessed'\n | 'secret.rotation.failed'\n | 'api_key.created'\n | 'api_key.revoked'\n | 'rate_limit.exceeded'\n | 'validation.failed'\n | 'webhook.signature.failed'\n | 'dependency.scan.failed'\n | 'llm.prompt_injection.detected'\n | 'llm.tool_call.denied'\n | 'llm.output.blocked'\n | 'llm.output.budget_exceeded'\n | 'llm.guard.triggered'\n | 'llm.action_chain.suspicious'\n | 'llm.manifest.suspicious'\n | 'llm.plan.risk.elevated';\n\nexport interface SecurityEventMetadata {\n /** Stable, dot-separated event name, e.g. `auth.login.failed`. */\n name: SuggestedSecurityEventName | (string & {});\n category: SecurityEventCategory;\n outcome: SecurityOutcome;\n /** Defaults to `info`. */\n severity?: SecuritySeverity;\n /** Stable identifier of the actor — an id or a `hashIdentifier()` digest, never raw PII. */\n actorId?: string;\n targetType?: string;\n targetId?: string;\n tenantId?: string;\n /** Short machine-readable reason, e.g. `invalid_password`. */\n reason?: string;\n [key: string]: unknown;\n}\n\nexport interface SecurityEventOptions {\n ctx?: AuditContext;\n /**\n * Security events are exempt from tail sampling by default —\n * an attack you sampled away is an attack you cannot investigate.\n * Pass `false` to opt out (e.g. very high-volume info events).\n */\n forceKeep?: boolean;\n emitNow?: boolean;\n logger?: RequestLogger;\n /**\n * Also increment the `autotel.security.events` counter\n * (attributes: event, category, outcome, severity) so security teams\n * can alert on rates without log-based alerting. Default true.\n *\n * Cardinality note: the event name is a counter attribute — keep names\n * to a stable catalogue, never interpolate user input into them.\n */\n metrics?: boolean;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: record nothing, warn once). A dropped security event is still\n * better than a crashed request — but the warning makes the gap visible.\n */\n onMissingContext?: OnMissingContext;\n}\n\nexport type WithSecurityOptions = SecurityEventOptions;\n\ninterface SecurityAttributeSink {\n setAttribute(\n key: string,\n value:\n | string\n | number\n | boolean\n | string[]\n | number[]\n | boolean[],\n ): unknown;\n}\n\n/**\n * Standard metadata fields and the schema attribute each maps to.\n * Drives both standard-field emission and the reserved-key check for the\n * custom-attribute loop — adding a field here is the whole change.\n */\nconst FIELD_ATTRIBUTES: Record<string, string> = {\n name: SECURITY_ATTR.event,\n category: SECURITY_ATTR.category,\n outcome: SECURITY_ATTR.outcome,\n severity: SECURITY_ATTR.severity,\n actorId: SECURITY_ATTR.actorId,\n targetType: SECURITY_ATTR.targetType,\n targetId: SECURITY_ATTR.targetId,\n tenantId: SECURITY_ATTR.tenantId,\n reason: SECURITY_ATTR.reason,\n};\n\nfunction flattenSecurityAttributes(\n metadata: SecurityEventMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n [SECURITY_ATTR.marker]: true,\n [SECURITY_ATTR.severity]: metadata.severity ?? 'info',\n };\n\n const droppedKeys: string[] = [];\n for (const [key, value] of Object.entries(metadata)) {\n const standardAttribute = FIELD_ATTRIBUTES[key];\n // Never emit values under credential-shaped custom keys, even by\n // accident. Reuses the core redactor's sensitive-key pattern so the\n // deny-list stays in one place.\n if (\n standardAttribute === undefined &&\n REDACTOR_PATTERNS.sensitiveKey.test(key)\n ) {\n droppedKeys.push(key);\n continue;\n }\n\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[standardAttribute ?? `security.${key}`] = attr;\n }\n }\n\n if (droppedKeys.length > 0) {\n attributes[SECURITY_ATTR.droppedKeys] = droppedKeys;\n }\n\n return attributes;\n}\n\nconst eventsCounter = lazyCounter(\n SECURITY_METRICS.events,\n 'Security events by name, category, outcome, and severity',\n);\n\nfunction countSecurityEvent(metadata: SecurityEventMetadata): void {\n eventsCounter.add(1, {\n event: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n });\n}\n\nexport function applySecurityEventAttributes(\n sink: SecurityAttributeSink,\n metadata: SecurityEventMetadata,\n options: Pick<SecurityEventOptions, 'forceKeep' | 'metrics'> = {},\n): void {\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (options.forceKeep !== false) {\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n sink.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n sink.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n\n for (const [key, value] of Object.entries(flattenSecurityAttributes(metadata))) {\n sink.setAttribute(key, value);\n }\n}\n\n/**\n * Record a security event on the active trace and request logger.\n *\n * Events are force-kept through tail sampling by default and carry\n * `security.*` attributes (`security.event`, `security.category`,\n * `security.outcome`, `security.severity`) so backends can build\n * detection rules and dashboards from a stable schema.\n *\n * ```typescript\n * securityEvent({\n * name: 'auth.login.failed',\n * category: 'authentication',\n * outcome: 'failure',\n * severity: 'warning',\n * actorId: hashIdentifier(email),\n * reason: 'invalid_password',\n * });\n * ```\n */\nexport function securityEvent(\n metadata: SecurityEventMetadata,\n options: SecurityEventOptions = {},\n): void {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // Counters are independent of trace context — always record the security signal\n // even when there's no span to attach attributes to.\n if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.name);\n }\n return;\n }\n\n if (options.forceKeep !== false) {\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute(SECURITY_ATTR.forceKeep, true);\n }\n traceCtx.setAttributes(flattenSecurityAttributes(metadata));\n\n const logger = options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n logger.set({\n security: {\n name: metadata.name,\n category: metadata.category,\n outcome: metadata.outcome,\n severity: metadata.severity ?? 'info',\n ...(metadata.reason !== undefined && { reason: metadata.reason }),\n forceKeep: options.forceKeep !== false,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n}\n\n/**\n * Wrap a security-sensitive operation. On success the event outcome is\n * recorded as given (default `success`); a thrown error records\n * `outcome: 'error'`, escalates the severity to at least `error`, and\n * rethrows.\n *\n * ```typescript\n * await withSecurity(\n * { name: 'api_key.created', category: 'secrets', outcome: 'success', actorId: userId },\n * async () => createApiKey(userId),\n * );\n * ```\n */\nexport async function withSecurity<T>(\n metadata: SecurityEventMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithSecurityOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n const logger =\n options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();\n const ctx = traceCtx ?? noopAuditContext();\n\n try {\n const result = await fn(ctx, logger);\n securityEvent(metadata, { ...options, ctx: traceCtx ?? undefined, logger });\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n securityEvent(\n {\n ...metadata,\n outcome: 'error',\n // A failed security-sensitive operation is never less than an error,\n // but an explicit `critical` stays critical.\n severity: escalateSecuritySeverity(metadata.severity ?? 'info', 'error'),\n },\n { ...options, ctx: traceCtx ?? undefined, logger },\n );\n logger.error(asError, {\n security: {\n name: metadata.name,\n category: metadata.category,\n },\n });\n throw asError;\n }\n}\n\nexport interface HashIdentifierOptions {\n /** Optional salt; use one stable per-deployment salt to defeat rainbow lookups. */\n salt?: string;\n /** Digest length in hex chars (default 16). */\n length?: number;\n}\n\n/**\n * Stable one-way digest for correlating PII-bearing identifiers\n * (emails, IPs) across events WITHOUT logging the raw value.\n *\n * NOT for secrets — never log secrets in any form, hashed or not.\n */\nexport function hashIdentifier(\n value: string,\n options: HashIdentifierOptions = {},\n): string {\n const length = options.length ?? 16;\n return createHash('sha256')\n .update(options.salt ? `${options.salt}:${value}` : value)\n .digest('hex')\n .slice(0, length);\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n} from 'autotel';\nimport {\n HTTP_STATUS_ATTRIBUTES,\n SECURITY_ATTR,\n SECURITY_DENIED_STATUSES,\n SECURITY_METRICS,\n} from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\nimport { applySecurityEventAttributes } from './security.js';\n\n/**\n * Zero-code security signal derivation from spans you already have.\n *\n * `createSecuritySignalProcessor()` watches ordinary HTTP server spans and:\n *\n * - flags suspicious request paths (traversal, `.env`/`.git` probes,\n * SQLi/XSS probes) at span start, marking them `security.suspicious_request`\n * and force-keeping them through tail sampling\n * - counts denied responses (401/403/429 by default) into the\n * `autotel.security.http.denied` metric\n * - detects auth-failure bursts per client (sliding window) and surfaces\n * them via the `autotel.security.anomaly` metric and an `onSignal` callback\n *\n * ```typescript\n * init({\n * service: 'api',\n * spanProcessors: [createSecuritySignalProcessor()],\n * });\n * ```\n *\n * Detection rules, alert thresholds, and dashboards belong in your\n * observability backend — this processor's job is to make the signals\n * exist, survive sampling, and stay queryable under a stable schema.\n */\n\n// Structural subset of @opentelemetry/sdk-trace-base types — kept local so\n// autotel-audit adds no new dependencies. Objects returned here satisfy the\n// real SpanProcessor interface structurally (must mirror @opentelemetry/api's\n// AttributeValue, including nullable array entries).\ntype AttributeValue =\n | string\n | number\n | boolean\n | Array<null | undefined | string>\n | Array<null | undefined | number>\n | Array<null | undefined | boolean>;\n\ninterface MutableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n setAttribute(key: string, value: AttributeValue): unknown;\n}\n\ninterface ReadableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\n spanContext?: { traceId: string };\n}\n\nexport interface SecuritySignalProcessor {\n onStart(span: MutableSpanLike, parentContext?: unknown): void;\n onEnd(span: ReadableSpanLike): void;\n shutdown(): Promise<void>;\n forceFlush(): Promise<void>;\n}\n\nexport interface SuspiciousRequestSignal {\n signal: 'suspicious_request';\n /** Which pattern matched, e.g. `path_traversal`. */\n pattern: string;\n /** The matched request path/URL (as found on the span). */\n target: string;\n}\n\nexport interface AuthFailureBurstSignal {\n signal: 'auth_failure_burst';\n /** Value of the configured key attribute (e.g. client address). */\n key: string;\n /** Denied responses observed inside the window. */\n count: number;\n windowMs: number;\n status: number;\n}\n\nexport interface LlmExcessiveTokensSignal {\n signal: 'llm_excessive_tokens';\n /** Total tokens consumed by the single LLM call. */\n tokens: number;\n maxTokens: number;\n model?: string;\n}\n\nexport interface LlmTokenBudgetSignal {\n signal: 'llm_token_budget_exceeded';\n /** Value of the configured key attribute (e.g. end-user id). */\n key: string;\n /** Tokens consumed inside the window. */\n tokens: number;\n budget: number;\n windowMs: number;\n}\n\nexport interface LlmActionChainSuspiciousSignal {\n signal: 'llm_action_chain_suspicious';\n /** Trace where the suspicious chain was observed. */\n traceId: string;\n /** Tool that followed untrusted-content processing. */\n toolName?: string;\n /** Milliseconds since the untrusted tool call on the same trace. */\n elapsedMs: number;\n untrustedTool?: string;\n}\n\nexport type SecuritySignal =\n | SuspiciousRequestSignal\n | AuthFailureBurstSignal\n | LlmExcessiveTokensSignal\n | LlmTokenBudgetSignal\n | LlmActionChainSuspiciousSignal;\n\nexport interface BurstOptions {\n /** HTTP statuses counted toward a burst. Default `[401, 403]`. */\n statuses?: number[];\n /** Denied responses within the window that trigger a signal. Default 10. */\n threshold?: number;\n /** Sliding window size in milliseconds. Default 60_000. */\n windowMs?: number;\n /**\n * Span attribute identifying the client. Default `client.address`\n * (falls back to `http.client_ip`).\n */\n keyAttribute?: string;\n /** Max distinct clients tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n}\n\nexport interface LlmSignalOptions {\n /**\n * Single-call token ceiling (`gen_ai.usage.total_tokens`, or input+output).\n * Default 100_000. Pass `false` to disable the per-call check.\n */\n maxTokensPerCall?: number | false;\n /**\n * Sliding-window token budget per key — catches slow-drip abuse that\n * stays under the per-call ceiling (OWASP LLM10: Unbounded Consumption).\n * Off unless configured.\n */\n tokenBudget?: {\n budget: number;\n /** Window size in milliseconds. Default 300_000 (5 min). */\n windowMs?: number;\n /**\n * Span attribute identifying the consumer. Default `enduser.id`\n * (falls back to `client.address`).\n */\n keyAttribute?: string;\n /** Max distinct keys tracked (oldest evicted). Default 10_000. */\n maxKeys?: number;\n };\n}\n\nexport interface SecuritySignalProcessorOptions {\n /** Flag suspicious request paths on span start. Default true. */\n detectSuspiciousRequests?: boolean;\n /** Additional name → pattern pairs checked against the request target. */\n extraPatterns?: Record<string, RegExp>;\n /** Force-keep flagged spans through tail sampling. Default true. */\n forceKeepSuspicious?: boolean;\n /** HTTP statuses counted as denied. Default `[401, 403, 429]`. */\n deniedStatuses?: number[];\n /** Burst detection over denied responses. Pass `false` to disable. */\n burst?: BurstOptions | false;\n /**\n * LLM consumption signals from `gen_ai.*` spans (OWASP LLM10).\n * Enabled with the per-call ceiling by default; pass `false` to disable.\n */\n llm?: LlmSignalOptions | false;\n /**\n * Detect destructive MCP tool calls that follow untrusted-content tool usage\n * on the same trace (Google's \"read email then send externally\" pattern).\n * Default true.\n */\n detectSuspiciousActionChains?: boolean;\n /** Max ms between untrusted and destructive tool calls on one trace. Default 300_000. */\n actionChainWindowMs?: number;\n /** Emit `autotel.security.*` metrics. Default true. */\n metrics?: boolean;\n /** Called whenever a signal fires. Keep it fast and non-throwing. */\n onSignal?: (signal: SecuritySignal) => void;\n /** Clock override for tests. */\n now?: () => number;\n}\n\n/**\n * Conservative request-target patterns. Tuned for scanner/probe traffic —\n * high signal, low false-positive — not as a WAF. Extend via `extraPatterns`.\n */\nexport const SUSPICIOUS_REQUEST_PATTERNS: Record<string, RegExp> = {\n path_traversal: /(\\.\\.[/\\\\]|%2e%2e(%2f|%5c|\\/)|\\.\\.%2f|%252e%252e)/i,\n sensitive_file_probe:\n /(\\/\\.env\\b|\\/\\.git\\b|\\/etc\\/passwd|\\/wp-admin\\b|\\/\\.aws\\b|\\/id_rsa\\b)/i,\n sqli_probe:\n /(\\bunion\\b[\\s+%20]+(all[\\s+%20]+)?select\\b|'[\\s+%20]*or[\\s+%20]*'?1'?[\\s+%20]*=[\\s+%20]*'?1)/i,\n xss_probe: /(<script\\b|%3cscript)/i,\n null_byte: /%00/,\n};\n\nconst TARGET_ATTRIBUTES = [\n 'url.path',\n 'url.full',\n 'http.target',\n 'http.url',\n] as const;\n\nfunction readAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n keys: readonly string[],\n): AttributeValue | undefined {\n for (const key of keys) {\n const value = attributes[key];\n if (value !== undefined) return value;\n }\n return undefined;\n}\n\n/**\n * Weighted sliding-window counter with bounded key cardinality.\n * Weight 1 per hit counts occurrences; token counts as weights sum usage.\n */\nclass SlidingWindow {\n private readonly hits = new Map<string, Array<[number, number]>>();\n\n constructor(\n private readonly windowMs: number,\n private readonly maxKeys: number,\n ) {}\n\n /**\n * Record a hit; returns the totals inside the window before and after it,\n * so callers can signal exactly once on a threshold crossing.\n */\n record(key: string, now: number, weight = 1): { before: number; after: number } {\n let entries = this.hits.get(key);\n if (!entries) {\n // Bound memory: random client addresses must not grow the map forever.\n if (this.hits.size >= this.maxKeys) {\n const oldest = this.hits.keys().next().value;\n if (oldest !== undefined) this.hits.delete(oldest);\n }\n entries = [];\n this.hits.set(key, entries);\n }\n\n const cutoff = now - this.windowMs;\n while (entries.length > 0 && (entries[0] as [number, number])[0] < cutoff) {\n entries.shift();\n }\n\n let before = 0;\n for (const [, w] of entries) before += w;\n entries.push([now, weight]);\n return { before, after: before + weight };\n }\n}\n\n/** Burst detection options with defaults applied and the window attached. */\ninterface BurstConfig {\n statuses: Set<number>;\n threshold: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n}\n\nfunction resolveBurstConfig(\n option: BurstOptions | false | undefined,\n): BurstConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const windowMs = opts.windowMs ?? 60_000;\n return {\n statuses: new Set(opts.statuses ?? [401, 403]),\n threshold: opts.threshold ?? 10,\n windowMs,\n keyAttribute: opts.keyAttribute ?? 'client.address',\n window: new SlidingWindow(windowMs, opts.maxKeys ?? 10_000),\n };\n}\n\n/** LLM consumption options with defaults applied and windows attached. */\ninterface LlmConfig {\n maxTokensPerCall?: number;\n budget?: {\n budget: number;\n windowMs: number;\n keyAttribute: string;\n window: SlidingWindow;\n };\n}\n\nfunction resolveLlmConfig(\n option: LlmSignalOptions | false | undefined,\n): LlmConfig | undefined {\n if (option === false) return undefined;\n const opts = option ?? {};\n const tokenBudget = opts.tokenBudget;\n const windowMs = tokenBudget?.windowMs ?? 300_000;\n return {\n maxTokensPerCall:\n opts.maxTokensPerCall === false\n ? undefined\n : (opts.maxTokensPerCall ?? 100_000),\n budget: tokenBudget && {\n budget: tokenBudget.budget,\n windowMs,\n keyAttribute: tokenBudget.keyAttribute ?? 'enduser.id',\n window: new SlidingWindow(windowMs, tokenBudget.maxKeys ?? 10_000),\n },\n };\n}\n\nconst MCP_TOOL_UNTRUSTED = 'mcp.tool.untrusted_content';\nconst MCP_TOOL_DESTRUCTIVE = 'mcp.tool.destructive';\nconst MCP_TOOL_NAME = 'mcp.tool.name';\n\ninterface UntrustedToolHit {\n toolName?: string;\n timestamp: number;\n}\n\nfunction pruneExpiredUntrustedTraces(\n hits: Map<string, UntrustedToolHit>,\n cutoff: number,\n): void {\n for (const [traceId, hit] of hits) {\n if (hit.timestamp < cutoff) {\n hits.delete(traceId);\n }\n }\n}\n\nfunction readTraceId(span: ReadableSpanLike): string | undefined {\n const fromContext = span.spanContext?.traceId;\n if (typeof fromContext === 'string' && fromContext.length > 0) {\n return fromContext;\n }\n const fromAttr = readAttribute(span.attributes, ['trace_id']);\n return typeof fromAttr === 'string' && fromAttr.length > 0 ? fromAttr : undefined;\n}\n\nfunction readBooleanAttribute(\n attributes: Record<string, AttributeValue | undefined>,\n key: string,\n): boolean {\n return readAttribute(attributes, [key]) === true;\n}\n\nexport function createSecuritySignalProcessor(\n options: SecuritySignalProcessorOptions = {},\n): SecuritySignalProcessor {\n const detect = options.detectSuspiciousRequests !== false;\n const forceKeep = options.forceKeepSuspicious !== false;\n const metricsEnabled = options.metrics !== false;\n const deniedStatuses = new Set(\n options.deniedStatuses ?? SECURITY_DENIED_STATUSES,\n );\n const now = options.now ?? Date.now;\n\n const patterns: Record<string, RegExp> = {\n ...SUSPICIOUS_REQUEST_PATTERNS,\n ...options.extraPatterns,\n };\n\n const burst = resolveBurstConfig(options.burst);\n const llm = resolveLlmConfig(options.llm);\n const detectActionChains = options.detectSuspiciousActionChains !== false;\n const actionChainWindowMs = options.actionChainWindowMs ?? 300_000;\n const untrustedByTrace = new Map<string, UntrustedToolHit>();\n\n const counters = {\n suspicious: lazyCounter(\n SECURITY_METRICS.httpSuspicious,\n 'Requests matching suspicious-path patterns',\n ),\n denied: lazyCounter(\n SECURITY_METRICS.httpDenied,\n 'HTTP responses with denied status codes (401/403/429)',\n ),\n anomaly: lazyCounter(\n SECURITY_METRICS.anomaly,\n 'Security anomaly signals (e.g. auth-failure bursts)',\n ),\n };\n\n function count(\n which: keyof typeof counters,\n attributes: Record<string, string | number>,\n ): void {\n if (!metricsEnabled) return;\n counters[which].add(1, attributes);\n }\n\n function emit(signal: SecuritySignal): void {\n try {\n options.onSignal?.(signal);\n } catch {\n // Callbacks must never break the span pipeline.\n }\n }\n\n function checkDeniedResponse(span: ReadableSpanLike): void {\n const status = readAttribute(span.attributes, HTTP_STATUS_ATTRIBUTES);\n if (typeof status !== 'number' || !deniedStatuses.has(status)) return;\n\n count('denied', { status });\n\n if (!burst || !burst.statuses.has(status)) return;\n\n const key = readAttribute(span.attributes, [\n burst.keyAttribute,\n 'http.client_ip',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = burst.window.record(key, now());\n // Signal once per window on the exact crossing, not on every\n // subsequent hit — keeps anomaly volume bounded under attack.\n if (before < burst.threshold && after >= burst.threshold) {\n count('anomaly', { signal: 'auth_failure_burst', status });\n emit({\n signal: 'auth_failure_burst',\n key,\n count: after,\n windowMs: burst.windowMs,\n status,\n });\n }\n }\n\n function checkLlmConsumption(span: ReadableSpanLike): void {\n if (!llm) return;\n\n const total = readAttribute(span.attributes, ['gen_ai.usage.total_tokens']);\n let tokens: number | undefined;\n if (typeof total === 'number') {\n tokens = total;\n } else {\n const input = readAttribute(span.attributes, ['gen_ai.usage.input_tokens']);\n const output = readAttribute(span.attributes, [\n 'gen_ai.usage.output_tokens',\n ]);\n if (typeof input === 'number' || typeof output === 'number') {\n tokens =\n (typeof input === 'number' ? input : 0) +\n (typeof output === 'number' ? output : 0);\n }\n }\n if (tokens === undefined || tokens <= 0) return;\n\n if (llm.maxTokensPerCall !== undefined && tokens > llm.maxTokensPerCall) {\n const model = readAttribute(span.attributes, [\n 'gen_ai.response.model',\n 'gen_ai.request.model',\n ]);\n count('anomaly', { signal: 'llm_excessive_tokens' });\n emit({\n signal: 'llm_excessive_tokens',\n tokens,\n maxTokens: llm.maxTokensPerCall,\n ...(typeof model === 'string' && { model }),\n });\n }\n\n const budget = llm.budget;\n if (!budget) return;\n\n const key = readAttribute(span.attributes, [\n budget.keyAttribute,\n 'client.address',\n ]);\n if (typeof key !== 'string' || key.length === 0) return;\n\n const { before, after } = budget.window.record(key, now(), tokens);\n if (before < budget.budget && after >= budget.budget) {\n count('anomaly', { signal: 'llm_token_budget_exceeded' });\n emit({\n signal: 'llm_token_budget_exceeded',\n key,\n tokens: after,\n budget: budget.budget,\n windowMs: budget.windowMs,\n });\n }\n }\n\n function checkSuspiciousActionChain(span: MutableSpanLike): void {\n if (!detectActionChains) return;\n\n const traceId = readTraceId(span);\n if (!traceId) return;\n\n const nowMs = now();\n const cutoff = nowMs - actionChainWindowMs;\n pruneExpiredUntrustedTraces(untrustedByTrace, cutoff);\n\n if (readBooleanAttribute(span.attributes, MCP_TOOL_UNTRUSTED)) {\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n untrustedByTrace.set(traceId, {\n timestamp: nowMs,\n ...(typeof toolName === 'string' && { toolName }),\n });\n return;\n }\n\n if (!readBooleanAttribute(span.attributes, MCP_TOOL_DESTRUCTIVE)) {\n return;\n }\n\n const prior = untrustedByTrace.get(traceId);\n if (!prior) {\n return;\n }\n untrustedByTrace.delete(traceId);\n\n const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);\n const elapsedMs = nowMs - prior.timestamp;\n count('anomaly', { signal: 'llm_action_chain_suspicious' });\n emit({\n signal: 'llm_action_chain_suspicious',\n traceId,\n elapsedMs,\n ...(typeof toolName === 'string' && { toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n });\n applySecurityEventAttributes(\n span,\n {\n name: 'llm.action_chain.suspicious',\n category: 'llm',\n outcome: 'denied',\n severity: 'warning',\n reason: 'untrusted_then_destructive',\n targetType: 'trace',\n targetId: traceId,\n ...(typeof toolName === 'string' && { destructiveTool: toolName }),\n ...(prior.toolName !== undefined && { untrustedTool: prior.toolName }),\n elapsedMs,\n },\n { forceKeep: true, metrics: false },\n );\n }\n\n return {\n onStart(span) {\n checkSuspiciousActionChain(span);\n if (!detect) return;\n\n const target = readAttribute(span.attributes, TARGET_ATTRIBUTES);\n if (typeof target !== 'string' || target.length === 0) return;\n\n for (const [name, pattern] of Object.entries(patterns)) {\n if (!pattern.test(target)) continue;\n\n span.setAttribute(SECURITY_ATTR.suspiciousRequest, true);\n span.setAttribute(SECURITY_ATTR.signal, name);\n if (forceKeep) {\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n }\n\n count('suspicious', { pattern: name });\n emit({ signal: 'suspicious_request', pattern: name, target });\n return; // first match wins — one signal per span\n }\n },\n\n onEnd(span) {\n checkDeniedResponse(span);\n checkLlmConsumption(span);\n },\n\n shutdown() {\n return Promise.resolve();\n },\n\n forceFlush() {\n return Promise.resolve();\n },\n };\n}\n","import { SECURITY_METRICS } from 'autotel/security-schema';\nimport { lazyCounter } from './lazy-counter';\n\n/**\n * Security-telemetry heartbeat.\n *\n * A silently-dead telemetry pipeline is itself a security failure (NIST\n * SP 800-92: systems must not keep operating without visibility into\n * security events). `startSecurityHeartbeat()` emits the\n * `autotel.security.heartbeat` counter on a fixed interval so security\n * teams can alert on the ABSENCE of telemetry from a service:\n *\n * ```promql\n * absent(rate(autotel_security_heartbeat_total{service_name=\"api\"}[5m]))\n * ```\n *\n * ```typescript\n * const heartbeat = startSecurityHeartbeat();\n * // on shutdown:\n * heartbeat.stop();\n * ```\n */\n\nexport interface SecurityHeartbeatOptions {\n /** Beat interval in milliseconds. Default 60_000. */\n intervalMs?: number;\n /** Extra counter attributes (keep cardinality low — labels, not data). */\n attributes?: Record<string, string | number | boolean>;\n}\n\nexport interface SecurityHeartbeat {\n stop(): void;\n}\n\nexport function startSecurityHeartbeat(\n options: SecurityHeartbeatOptions = {},\n): SecurityHeartbeat {\n const intervalMs = options.intervalMs ?? 60_000;\n const attributes = options.attributes ?? {};\n\n const counter = lazyCounter(\n SECURITY_METRICS.heartbeat,\n 'Security-telemetry liveness signal — alert on its absence',\n );\n\n function beat(): void {\n counter.add(1, attributes);\n }\n\n beat(); // establish the series immediately, not one interval later\n const timer = setInterval(beat, intervalMs);\n // Never hold the process open just to beat.\n timer.unref?.();\n\n let stopped = false;\n return {\n stop() {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n },\n };\n}\n","import type { AuditContext } from './context.js';\nimport {\n securityEvent,\n type SecurityEventMetadata,\n type SecurityEventOptions,\n} from './security.js';\n\n/**\n * Metadata emitted when MCP protocol-boundary signals are bridged to the\n * unified `security.*` schema. Used by `autotel-mcp-instrumentation` when\n * `bridgeSecurityEvents` is enabled.\n */\nexport interface McpBridgedSecurityEvent {\n name: SecurityEventMetadata['name'];\n category: 'llm';\n outcome: SecurityEventMetadata['outcome'];\n severity?: SecurityEventMetadata['severity'];\n reason?: string;\n toolName?: string;\n verdict?: string;\n source?: string;\n [key: string]: unknown;\n}\n\nexport interface McpSecurityEventBridgeOptions extends SecurityEventOptions {\n /** Optional fixed audit context for bridged events. */\n ctx?: AuditContext;\n}\n\n/**\n * Create a bridge callback for MCP security observability → `securityEvent()`.\n *\n * @example\n * ```typescript\n * import { createMcpSecurityEventBridge } from 'autotel-audit';\n * import { instrumentMcpClient } from 'autotel-mcp-instrumentation/client';\n *\n * instrumentMcpClient(client, {\n * securityClassifier: heuristicInjectionClassifier(),\n * bridgeSecurityEvents: true,\n * securityEventBridge: createMcpSecurityEventBridge(),\n * });\n * ```\n */\nexport function createMcpSecurityEventBridge(\n options: McpSecurityEventBridgeOptions = {},\n): (metadata: McpBridgedSecurityEvent) => void {\n return (metadata) => {\n const { toolName, verdict, source, ...rest } = metadata;\n securityEvent(\n {\n ...rest,\n category: 'llm',\n ...(toolName !== undefined && { targetType: 'tool', targetId: toolName }),\n ...(verdict !== undefined && { verdict }),\n ...(source !== undefined && { source }),\n },\n options,\n );\n };\n}\n","import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n createNoopRequestLogger,\n getRequestLoggerSafe,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport {\n MISSING_CONTEXT_MESSAGE,\n noopAuditContext,\n resolveContextSafe,\n toAttributeValue,\n warnMissingContextOnce,\n warnMissingLoggerOnce,\n type AuditContext,\n type OnMissingContext,\n} from './context';\n\nexport type { AuditContext, OnMissingContext } from './context';\nexport * from './security';\nexport * from './security-signals';\nexport * from './security-heartbeat';\nexport * from './mcp-bridge';\n\nexport interface AuditMetadata {\n action: string;\n resource?: string;\n actorId?: string;\n category?: string;\n outcome?: 'success' | 'failure' | (string & {});\n [key: string]: unknown;\n}\n\nexport interface WithAuditOptions {\n ctx?: AuditContext;\n emitNow?: boolean;\n forceKeep?: boolean;\n logger?: RequestLogger;\n /**\n * Behaviour when no trace context can be resolved. Defaults to `warn`\n * (best-effort: run un-audited, warn once). See {@link OnMissingContext}.\n */\n onMissingContext?: OnMissingContext;\n}\n\nfunction flattenAuditAttributes(\n metadata: AuditMetadata,\n): Record<string, string | number | boolean | string[] | number[] | boolean[]> {\n const attributes: Record<\n string,\n string | number | boolean | string[] | number[] | boolean[]\n > = {\n 'autotel.audit': true,\n };\n\n for (const [key, value] of Object.entries(metadata)) {\n const attr = toAttributeValue(value);\n if (attr !== undefined) {\n attributes[`audit.${key}`] = attr;\n }\n }\n\n return attributes;\n}\n\nexport function forceKeepAuditEvent(ctx?: AuditContext): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);\n traceCtx.setAttribute('autotel.audit.force_keep', true);\n}\n\nexport function setAuditAttributes(\n metadata: AuditMetadata,\n ctx?: AuditContext,\n): void {\n const traceCtx = resolveContextSafe(ctx);\n if (!traceCtx) return;\n traceCtx.setAttributes(flattenAuditAttributes(metadata));\n}\n\nexport async function withAudit<T>(\n metadata: AuditMetadata,\n fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,\n options: WithAuditOptions = {},\n): Promise<T> {\n const traceCtx = resolveContextSafe(options.ctx);\n\n // No trace context: degrade per onMissingContext instead of throwing into\n // business logic. Audit is observability — it must never crash the caller.\n if (!traceCtx) {\n const mode = options.onMissingContext ?? 'warn';\n if (mode === 'throw') {\n throw new Error(MISSING_CONTEXT_MESSAGE);\n }\n if (mode === 'warn') {\n warnMissingContextOnce(metadata.action);\n }\n return fn(noopAuditContext(), options.logger ?? createNoopRequestLogger());\n }\n\n if (options.forceKeep !== false) {\n forceKeepAuditEvent(traceCtx);\n }\n\n setAuditAttributes(metadata, traceCtx);\n\n // A trace context may exist (e.g. caller-supplied options.ctx) without a\n // resolvable request logger. Record span attributes regardless and only skip\n // the canonical log line — never throw.\n let logger = options.logger ?? getRequestLoggerSafe() ?? undefined;\n if (!logger) {\n if ((options.onMissingContext ?? 'warn') === 'warn') {\n warnMissingLoggerOnce(metadata.action);\n }\n logger = createNoopRequestLogger();\n }\n logger.set({\n audit: {\n ...metadata,\n forceKeep: options.forceKeep !== false,\n },\n });\n\n try {\n const result = await fn(traceCtx, logger);\n\n if (!metadata.outcome) {\n setAuditAttributes({ ...metadata, outcome: 'success' }, traceCtx);\n }\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n return result;\n } catch (error) {\n const asError = error instanceof Error ? error : new Error(String(error));\n setAuditAttributes({ ...metadata, outcome: 'failure' }, traceCtx);\n logger.error(asError, {\n audit: {\n action: metadata.action,\n resource: metadata.resource,\n },\n });\n\n if (options.emitNow) {\n logger.emitNow();\n }\n\n throw asError;\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel-audit",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Audit-focused helpers for Autotel (force-keep + structured audit instrumentation)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,20 +25,20 @@
25
25
  "tail-sampling"
26
26
  ],
27
27
  "author": "Jag Reehal <jag@jagreehal.com> (https://jagreehal.com)",
28
- "license": "MIT",
28
+ "license": "Apache-2.0",
29
29
  "dependencies": {
30
- "autotel": "4.2.3"
30
+ "autotel": "4.2.5"
31
31
  },
32
32
  "devDependencies": {
33
- "@types/node": "^25.9.2",
34
- "@typescript-eslint/eslint-plugin": "^8.60.1",
35
- "@typescript-eslint/parser": "^8.60.1",
33
+ "@types/node": "^26.1.0",
34
+ "@typescript-eslint/eslint-plugin": "^8.63.0",
35
+ "@typescript-eslint/parser": "^8.63.0",
36
36
  "eslint-config-prettier": "^10.1.8",
37
37
  "eslint-plugin-unicorn": "^64.0.0",
38
- "tsdown": "^0.22.2",
38
+ "tsdown": "^0.22.3",
39
39
  "typescript": "^6.0.3",
40
- "typescript-eslint": "^8.60.1",
41
- "vitest": "^4.1.8"
40
+ "typescript-eslint": "^8.63.0",
41
+ "vitest": "^4.1.10"
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",