autotel-audit 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -32,7 +32,7 @@ type SecurityOutcome = 'success' | 'failure' | 'denied' | 'blocked' | 'error';
32
32
  * Well-known security event names. Free-form names are allowed —
33
33
  * this union exists for autocomplete and consistency across services.
34
34
  */
35
- type SuggestedSecurityEventName = 'auth.login.success' | 'auth.login.failed' | 'auth.mfa.failed' | 'auth.session.revoked' | 'auth.password.reset' | 'auth.account.locked' | 'access.denied' | 'access.role.changed' | 'access.permission.changed' | 'access.tenant.violation' | 'admin.action' | 'config.changed' | 'secret.accessed' | 'secret.rotation.failed' | 'api_key.created' | 'api_key.revoked' | 'rate_limit.exceeded' | 'validation.failed' | 'webhook.signature.failed' | 'dependency.scan.failed' | 'llm.prompt_injection.detected' | 'llm.tool_call.denied' | 'llm.output.blocked';
35
+ type SuggestedSecurityEventName = 'auth.login.success' | 'auth.login.failed' | 'auth.mfa.failed' | 'auth.session.revoked' | 'auth.password.reset' | 'auth.account.locked' | 'access.denied' | 'access.role.changed' | 'access.permission.changed' | 'access.tenant.violation' | 'admin.action' | 'config.changed' | 'secret.accessed' | 'secret.rotation.failed' | 'api_key.created' | 'api_key.revoked' | 'rate_limit.exceeded' | 'validation.failed' | 'webhook.signature.failed' | 'dependency.scan.failed' | 'llm.prompt_injection.detected' | 'llm.tool_call.denied' | 'llm.output.blocked' | 'llm.output.budget_exceeded' | 'llm.guard.triggered' | 'llm.action_chain.suspicious' | 'llm.manifest.suspicious' | 'llm.plan.risk.elevated';
36
36
  interface SecurityEventMetadata {
37
37
  /** Stable, dot-separated event name, e.g. `auth.login.failed`. */
38
38
  name: SuggestedSecurityEventName | (string & {});
@@ -76,6 +76,10 @@ interface SecurityEventOptions {
76
76
  onMissingContext?: OnMissingContext;
77
77
  }
78
78
  type WithSecurityOptions = SecurityEventOptions;
79
+ interface SecurityAttributeSink {
80
+ setAttribute(key: string, value: string | number | boolean | string[] | number[] | boolean[]): unknown;
81
+ }
82
+ declare function applySecurityEventAttributes(sink: SecurityAttributeSink, metadata: SecurityEventMetadata, options?: Pick<SecurityEventOptions, 'forceKeep' | 'metrics'>): void;
79
83
  /**
80
84
  * Record a security event on the active trace and request logger.
81
85
  *
@@ -152,10 +156,16 @@ declare function hashIdentifier(value: string, options?: HashIdentifierOptions):
152
156
  type AttributeValue = string | number | boolean | Array<null | undefined | string> | Array<null | undefined | number> | Array<null | undefined | boolean>;
153
157
  interface MutableSpanLike {
154
158
  attributes: Record<string, AttributeValue | undefined>;
159
+ spanContext?: {
160
+ traceId: string;
161
+ };
155
162
  setAttribute(key: string, value: AttributeValue): unknown;
156
163
  }
157
164
  interface ReadableSpanLike {
158
165
  attributes: Record<string, AttributeValue | undefined>;
166
+ spanContext?: {
167
+ traceId: string;
168
+ };
159
169
  }
160
170
  interface SecuritySignalProcessor {
161
171
  onStart(span: MutableSpanLike, parentContext?: unknown): void;
@@ -195,7 +205,17 @@ interface LlmTokenBudgetSignal {
195
205
  budget: number;
196
206
  windowMs: number;
197
207
  }
198
- type SecuritySignal = SuspiciousRequestSignal | AuthFailureBurstSignal | LlmExcessiveTokensSignal | LlmTokenBudgetSignal;
208
+ interface LlmActionChainSuspiciousSignal {
209
+ signal: 'llm_action_chain_suspicious';
210
+ /** Trace where the suspicious chain was observed. */
211
+ traceId: string;
212
+ /** Tool that followed untrusted-content processing. */
213
+ toolName?: string;
214
+ /** Milliseconds since the untrusted tool call on the same trace. */
215
+ elapsedMs: number;
216
+ untrustedTool?: string;
217
+ }
218
+ type SecuritySignal = SuspiciousRequestSignal | AuthFailureBurstSignal | LlmExcessiveTokensSignal | LlmTokenBudgetSignal | LlmActionChainSuspiciousSignal;
199
219
  interface BurstOptions {
200
220
  /** HTTP statuses counted toward a burst. Default `[401, 403]`. */
201
221
  statuses?: number[];
@@ -249,6 +269,14 @@ interface SecuritySignalProcessorOptions {
249
269
  * Enabled with the per-call ceiling by default; pass `false` to disable.
250
270
  */
251
271
  llm?: LlmSignalOptions | false;
272
+ /**
273
+ * Detect destructive MCP tool calls that follow untrusted-content tool usage
274
+ * on the same trace (Google's "read email then send externally" pattern).
275
+ * Default true.
276
+ */
277
+ detectSuspiciousActionChains?: boolean;
278
+ /** Max ms between untrusted and destructive tool calls on one trace. Default 300_000. */
279
+ actionChainWindowMs?: number;
252
280
  /** Emit `autotel.security.*` metrics. Default true. */
253
281
  metrics?: boolean;
254
282
  /** Called whenever a signal fires. Keep it fast and non-throwing. */
@@ -294,6 +322,44 @@ interface SecurityHeartbeat {
294
322
  }
295
323
  declare function startSecurityHeartbeat(options?: SecurityHeartbeatOptions): SecurityHeartbeat;
296
324
  //#endregion
325
+ //#region src/mcp-bridge.d.ts
326
+ /**
327
+ * Metadata emitted when MCP protocol-boundary signals are bridged to the
328
+ * unified `security.*` schema. Used by `autotel-mcp-instrumentation` when
329
+ * `bridgeSecurityEvents` is enabled.
330
+ */
331
+ interface McpBridgedSecurityEvent {
332
+ name: SecurityEventMetadata['name'];
333
+ category: 'llm';
334
+ outcome: SecurityEventMetadata['outcome'];
335
+ severity?: SecurityEventMetadata['severity'];
336
+ reason?: string;
337
+ toolName?: string;
338
+ verdict?: string;
339
+ source?: string;
340
+ [key: string]: unknown;
341
+ }
342
+ interface McpSecurityEventBridgeOptions extends SecurityEventOptions {
343
+ /** Optional fixed audit context for bridged events. */
344
+ ctx?: AuditContext;
345
+ }
346
+ /**
347
+ * Create a bridge callback for MCP security observability → `securityEvent()`.
348
+ *
349
+ * @example
350
+ * ```typescript
351
+ * import { createMcpSecurityEventBridge } from 'autotel-audit';
352
+ * import { instrumentMcpClient } from 'autotel-mcp-instrumentation/client';
353
+ *
354
+ * instrumentMcpClient(client, {
355
+ * securityClassifier: heuristicInjectionClassifier(),
356
+ * bridgeSecurityEvents: true,
357
+ * securityEventBridge: createMcpSecurityEventBridge(),
358
+ * });
359
+ * ```
360
+ */
361
+ declare function createMcpSecurityEventBridge(options?: McpSecurityEventBridgeOptions): (metadata: McpBridgedSecurityEvent) => void;
362
+ //#endregion
297
363
  //#region src/index.d.ts
298
364
  interface AuditMetadata {
299
365
  action: string;
@@ -318,5 +384,5 @@ declare function forceKeepAuditEvent(ctx?: AuditContext): void;
318
384
  declare function setAuditAttributes(metadata: AuditMetadata, ctx?: AuditContext): void;
319
385
  declare function withAudit<T>(metadata: AuditMetadata, fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>, options?: WithAuditOptions): Promise<T>;
320
386
  //#endregion
321
- export { type AuditContext, AuditMetadata, AuthFailureBurstSignal, BurstOptions, HashIdentifierOptions, LlmExcessiveTokensSignal, LlmSignalOptions, LlmTokenBudgetSignal, type OnMissingContext, SUSPICIOUS_REQUEST_PATTERNS, SecurityEventCategory, SecurityEventMetadata, SecurityEventOptions, SecurityHeartbeat, SecurityHeartbeatOptions, SecurityOutcome, type SecuritySeverity, SecuritySignal, SecuritySignalProcessor, SecuritySignalProcessorOptions, SuggestedSecurityEventName, SuspiciousRequestSignal, WithAuditOptions, WithSecurityOptions, createSecuritySignalProcessor, forceKeepAuditEvent, hashIdentifier, securityEvent, setAuditAttributes, startSecurityHeartbeat, withAudit, withSecurity };
387
+ export { type AuditContext, AuditMetadata, AuthFailureBurstSignal, BurstOptions, HashIdentifierOptions, LlmActionChainSuspiciousSignal, LlmExcessiveTokensSignal, LlmSignalOptions, LlmTokenBudgetSignal, McpBridgedSecurityEvent, McpSecurityEventBridgeOptions, type OnMissingContext, SUSPICIOUS_REQUEST_PATTERNS, SecurityEventCategory, SecurityEventMetadata, SecurityEventOptions, SecurityHeartbeat, SecurityHeartbeatOptions, SecurityOutcome, type SecuritySeverity, SecuritySignal, SecuritySignalProcessor, SecuritySignalProcessorOptions, SuggestedSecurityEventName, SuspiciousRequestSignal, WithAuditOptions, WithSecurityOptions, applySecurityEventAttributes, createMcpSecurityEventBridge, createSecuritySignalProcessor, forceKeepAuditEvent, hashIdentifier, securityEvent, setAuditAttributes, startSecurityHeartbeat, withAudit, withSecurity };
322
388
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/context.ts","../src/security.ts","../src/security-signals.ts","../src/security-heartbeat.ts","../src/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/context.ts","../src/security.ts","../src/security-signals.ts","../src/security-heartbeat.ts","../src/mcp-bridge.ts","../src/index.ts"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -132,6 +132,15 @@ function countSecurityEvent(metadata) {
132
132
  severity: metadata.severity ?? "info"
133
133
  });
134
134
  }
135
+ function applySecurityEventAttributes(sink, metadata, options = {}) {
136
+ if (options.metrics !== false) countSecurityEvent(metadata);
137
+ if (options.forceKeep !== false) {
138
+ sink.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
139
+ sink.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, true);
140
+ sink.setAttribute(SECURITY_ATTR.forceKeep, true);
141
+ }
142
+ for (const [key, value] of Object.entries(flattenSecurityAttributes(metadata))) sink.setAttribute(key, value);
143
+ }
135
144
  /**
136
145
  * Record a security event on the active trace and request logger.
137
146
  *
@@ -320,6 +329,21 @@ function resolveLlmConfig(option) {
320
329
  }
321
330
  };
322
331
  }
332
+ const MCP_TOOL_UNTRUSTED = "mcp.tool.untrusted_content";
333
+ const MCP_TOOL_DESTRUCTIVE = "mcp.tool.destructive";
334
+ const MCP_TOOL_NAME = "mcp.tool.name";
335
+ function pruneExpiredUntrustedTraces(hits, cutoff) {
336
+ for (const [traceId, hit] of hits) if (hit.timestamp < cutoff) hits.delete(traceId);
337
+ }
338
+ function readTraceId(span) {
339
+ const fromContext = span.spanContext?.traceId;
340
+ if (typeof fromContext === "string" && fromContext.length > 0) return fromContext;
341
+ const fromAttr = readAttribute(span.attributes, ["trace_id"]);
342
+ return typeof fromAttr === "string" && fromAttr.length > 0 ? fromAttr : void 0;
343
+ }
344
+ function readBooleanAttribute(attributes, key) {
345
+ return readAttribute(attributes, [key]) === true;
346
+ }
323
347
  function createSecuritySignalProcessor(options = {}) {
324
348
  const detect = options.detectSuspiciousRequests !== false;
325
349
  const forceKeep = options.forceKeepSuspicious !== false;
@@ -332,6 +356,9 @@ function createSecuritySignalProcessor(options = {}) {
332
356
  };
333
357
  const burst = resolveBurstConfig(options.burst);
334
358
  const llm = resolveLlmConfig(options.llm);
359
+ const detectActionChains = options.detectSuspiciousActionChains !== false;
360
+ const actionChainWindowMs = options.actionChainWindowMs ?? 3e5;
361
+ const untrustedByTrace = /* @__PURE__ */ new Map();
335
362
  const counters = {
336
363
  suspicious: lazyCounter(SECURITY_METRICS.httpSuspicious, "Requests matching suspicious-path patterns"),
337
364
  denied: lazyCounter(SECURITY_METRICS.httpDenied, "HTTP responses with denied status codes (401/403/429)"),
@@ -405,8 +432,53 @@ function createSecuritySignalProcessor(options = {}) {
405
432
  });
406
433
  }
407
434
  }
435
+ function checkSuspiciousActionChain(span) {
436
+ if (!detectActionChains) return;
437
+ const traceId = readTraceId(span);
438
+ if (!traceId) return;
439
+ const nowMs = now();
440
+ pruneExpiredUntrustedTraces(untrustedByTrace, nowMs - actionChainWindowMs);
441
+ if (readBooleanAttribute(span.attributes, MCP_TOOL_UNTRUSTED)) {
442
+ const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);
443
+ untrustedByTrace.set(traceId, {
444
+ timestamp: nowMs,
445
+ ...typeof toolName === "string" && { toolName }
446
+ });
447
+ return;
448
+ }
449
+ if (!readBooleanAttribute(span.attributes, MCP_TOOL_DESTRUCTIVE)) return;
450
+ const prior = untrustedByTrace.get(traceId);
451
+ if (!prior) return;
452
+ untrustedByTrace.delete(traceId);
453
+ const toolName = readAttribute(span.attributes, [MCP_TOOL_NAME]);
454
+ const elapsedMs = nowMs - prior.timestamp;
455
+ count("anomaly", { signal: "llm_action_chain_suspicious" });
456
+ emit({
457
+ signal: "llm_action_chain_suspicious",
458
+ traceId,
459
+ elapsedMs,
460
+ ...typeof toolName === "string" && { toolName },
461
+ ...prior.toolName !== void 0 && { untrustedTool: prior.toolName }
462
+ });
463
+ applySecurityEventAttributes(span, {
464
+ name: "llm.action_chain.suspicious",
465
+ category: "llm",
466
+ outcome: "denied",
467
+ severity: "warning",
468
+ reason: "untrusted_then_destructive",
469
+ targetType: "trace",
470
+ targetId: traceId,
471
+ ...typeof toolName === "string" && { destructiveTool: toolName },
472
+ ...prior.toolName !== void 0 && { untrustedTool: prior.toolName },
473
+ elapsedMs
474
+ }, {
475
+ forceKeep: true,
476
+ metrics: false
477
+ });
478
+ }
408
479
  return {
409
480
  onStart(span) {
481
+ checkSuspiciousActionChain(span);
410
482
  if (!detect) return;
411
483
  const target = readAttribute(span.attributes, TARGET_ATTRIBUTES);
412
484
  if (typeof target !== "string" || target.length === 0) return;
@@ -460,6 +532,39 @@ function startSecurityHeartbeat(options = {}) {
460
532
  } };
461
533
  }
462
534
 
535
+ //#endregion
536
+ //#region src/mcp-bridge.ts
537
+ /**
538
+ * Create a bridge callback for MCP security observability → `securityEvent()`.
539
+ *
540
+ * @example
541
+ * ```typescript
542
+ * import { createMcpSecurityEventBridge } from 'autotel-audit';
543
+ * import { instrumentMcpClient } from 'autotel-mcp-instrumentation/client';
544
+ *
545
+ * instrumentMcpClient(client, {
546
+ * securityClassifier: heuristicInjectionClassifier(),
547
+ * bridgeSecurityEvents: true,
548
+ * securityEventBridge: createMcpSecurityEventBridge(),
549
+ * });
550
+ * ```
551
+ */
552
+ function createMcpSecurityEventBridge(options = {}) {
553
+ return (metadata) => {
554
+ const { toolName, verdict, source, ...rest } = metadata;
555
+ securityEvent({
556
+ ...rest,
557
+ category: "llm",
558
+ ...toolName !== void 0 && {
559
+ targetType: "tool",
560
+ targetId: toolName
561
+ },
562
+ ...verdict !== void 0 && { verdict },
563
+ ...source !== void 0 && { source }
564
+ }, options);
565
+ };
566
+ }
567
+
463
568
  //#endregion
464
569
  //#region src/index.ts
465
570
  function flattenAuditAttributes(metadata) {
@@ -525,5 +630,5 @@ async function withAudit(metadata, fn, options = {}) {
525
630
  }
526
631
 
527
632
  //#endregion
528
- export { SUSPICIOUS_REQUEST_PATTERNS, createSecuritySignalProcessor, forceKeepAuditEvent, hashIdentifier, securityEvent, setAuditAttributes, startSecurityHeartbeat, withAudit, withSecurity };
633
+ export { SUSPICIOUS_REQUEST_PATTERNS, applySecurityEventAttributes, createMcpSecurityEventBridge, createSecuritySignalProcessor, forceKeepAuditEvent, hashIdentifier, securityEvent, setAuditAttributes, startSecurityHeartbeat, withAudit, withSecurity };
529
634
  //# sourceMappingURL=index.js.map
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/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\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\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\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\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';\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 setAttribute(key: string, value: AttributeValue): unknown;\n}\n\ninterface ReadableSpanLike {\n attributes: Record<string, AttributeValue | undefined>;\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 type SecuritySignal =\n | SuspiciousRequestSignal\n | AuthFailureBurstSignal\n | LlmExcessiveTokensSignal\n | LlmTokenBudgetSignal;\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 /** 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\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\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 return {\n onStart(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 {\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';\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.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Audit-focused helpers for Autotel (force-keep + structured audit instrumentation)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,7 +15,6 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
- "src",
19
18
  "README.md"
20
19
  ],
21
20
  "keywords": [
@@ -28,7 +27,7 @@
28
27
  "author": "Jag Reehal <jag@jagreehal.com> (https://jagreehal.com)",
29
28
  "license": "MIT",
30
29
  "dependencies": {
31
- "autotel": "4.0.0"
30
+ "autotel": "4.2.0"
32
31
  },
33
32
  "devDependencies": {
34
33
  "@types/node": "^25.9.2",