autotel-audit 0.1.16 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,15 @@
1
1
  # autotel-audit
2
2
 
3
- Audit-focused helpers for `autotel`. Provides structured audit logging with automatic tail-sampling bypass and OpenTelemetry attribute normalization.
3
+ Audit and security-event helpers for `autotel`. Provides structured audit logging with automatic tail-sampling bypass and OpenTelemetry attribute normalization.
4
4
 
5
5
  ## What it provides
6
6
 
7
7
  - **`withAudit(...)`** — Wraps an operation with audit metadata, automatic outcome tagging (success/failure), and optional immediate emit
8
8
  - **`forceKeepAuditEvent(...)`** — Marks the current trace to bypass tail-drop sampling for compliance/audit trails
9
9
  - **`setAuditAttributes(...)`** — Writes normalized `audit.*` attributes on the active span with automatic type conversion
10
+ - **`securityEvent(...)`** / **`withSecurity(...)`** — Typed security events (OWASP A09-aligned) with a stable `security.*` schema, force-keep by default, a credential-key guard, and automatic counter metrics
11
+ - **`createSecuritySignalProcessor(...)`** — Zero-code security signals derived from the HTTP spans you already have: suspicious-path flagging, denied-response metrics, and per-client auth-failure burst detection
12
+ - **`hashIdentifier(...)`** — Stable one-way digest for correlating PII-bearing identifiers (emails, IPs) without logging raw values
10
13
 
11
14
  ## Features
12
15
 
@@ -208,6 +211,110 @@ if (isPrivilegedOperation) {
208
211
  }
209
212
  ```
210
213
 
214
+ ## Security Events (OWASP A09)
215
+
216
+ OWASP A09:2025 (Security Logging & Alerting Failures) calls out missing logs for important events, unclear messages, and ineffective alerting. `securityEvent()` gives security-relevant behaviours a **stable, queryable schema** so backends can build detection rules and dashboards instead of grepping free text.
217
+
218
+ ```ts
219
+ import { securityEvent, hashIdentifier } from 'autotel-audit';
220
+
221
+ // Inside a trace()-wrapped handler:
222
+ securityEvent({
223
+ name: 'auth.login.failed', // autocomplete for well-known names, free-form allowed
224
+ category: 'authentication',
225
+ outcome: 'failure',
226
+ severity: 'warning',
227
+ actorId: hashIdentifier(email), // correlate without logging PII
228
+ reason: 'invalid_password',
229
+ });
230
+ // Sets: security.event, security.category, security.outcome, security.severity,
231
+ // security.actor_id, security.reason, autotel.security=true
232
+ ```
233
+
234
+ ### Design guarantees
235
+
236
+ - **Force-keep by default** — an attack you sampled away is an attack you cannot investigate. Opt out per-event with `forceKeep: false` for high-volume info events.
237
+ - **Credential-key guard** — values under credential-shaped keys (`token`, `apiKey`, `password`, …, reusing autotel core's `REDACTOR_PATTERNS.sensitiveKey`) are never emitted, even by accident. Dropped key names are recorded in `security.dropped_keys`.
238
+ - **Stable schema** — `security.event` / `security.category` / `security.outcome` / `security.severity` are always present; everything else flattens under `security.*`.
239
+ - **Hash, don't log** — `hashIdentifier(value, { salt })` produces a stable sha256 digest for emails/IPs. Never log secrets, hashed or not.
240
+
241
+ ### Wrapping security-sensitive operations
242
+
243
+ ```ts
244
+ import { withSecurity } from 'autotel-audit';
245
+
246
+ await withSecurity(
247
+ {
248
+ name: 'api_key.created',
249
+ category: 'secrets',
250
+ outcome: 'success',
251
+ actorId: user.id,
252
+ keyId: newKey.id, // safe: identifier, not the key material
253
+ },
254
+ async () => createApiKey(user.id),
255
+ { emitNow: true },
256
+ );
257
+ // Success → outcome as given; thrown error → outcome: 'error',
258
+ // severity escalated to at least 'error', logged, rethrown
259
+ ```
260
+
261
+ ### Categories
262
+
263
+ `authentication` · `authorization` · `data_access` · `admin_action` · `configuration` · `secrets` · `rate_limit` · `validation` · `supply_chain` · `llm`
264
+
265
+ ### `securityEvent` vs `withAudit`
266
+
267
+ - **`withAudit`** — compliance trail for business operations ("who did what to which resource").
268
+ - **`securityEvent`** — detection signal for security-relevant behaviour ("is the system being abused"). Categories, outcomes, and severities are closed unions so alerting rules don't drift.
269
+
270
+ They compose: a sensitive admin operation can carry both an `audit.*` trail and a `security.*` event.
271
+
272
+ ### Metrics for alerting
273
+
274
+ Every `securityEvent()` also increments the `autotel.security.events` counter (attributes: `event`, `category`, `outcome`, `severity`) so security teams can alert on **rates** — failed-login spikes, denied-access bursts — without log-based alerting. Disable per-event with `metrics: false`.
275
+
276
+ > Cardinality: the event name is a counter attribute. Keep names to a stable catalogue; never interpolate user input into them.
277
+
278
+ ### Library vs backend responsibility
279
+
280
+ This package's job ends at emitting **structured, correlated, redaction-safe, sampling-exempt** events. Detection rules, alert thresholds, dashboards, and SIEM routing belong in your observability backend.
281
+
282
+ ## Zero-Code Security Signals
283
+
284
+ Most security-relevant traffic never reaches your handlers — scanners probing `/.env`, traversal attempts, credential stuffing producing 401 storms. `createSecuritySignalProcessor()` derives security signals from the **HTTP spans your instrumentation already produces**, with no per-route code:
285
+
286
+ ```ts
287
+ import { init } from 'autotel';
288
+ import { createSecuritySignalProcessor } from 'autotel-audit';
289
+
290
+ init({
291
+ service: 'api',
292
+ spanProcessors: [
293
+ createSecuritySignalProcessor({
294
+ onSignal: (signal) => {
295
+ // optional: forward to Slack/SIEM/pager
296
+ },
297
+ }),
298
+ ],
299
+ });
300
+ ```
301
+
302
+ What it does:
303
+
304
+ | Signal | When | Output |
305
+ |---|---|---|
306
+ | **Suspicious request** | Request path matches a probe pattern (path traversal, `/.env` / `/.git` / `/etc/passwd` probes, SQLi/XSS probes, null bytes) | Span flagged `security.suspicious_request=true` + `security.signal=<pattern>`, **force-kept through tail sampling**, `autotel.security.http.suspicious` counter, `onSignal` callback |
307
+ | **Denied response** | Response status is 401/403/429 (configurable) | `autotel.security.http.denied{status}` counter |
308
+ | **Auth-failure burst** | One client crosses N denied responses (default 10) inside a sliding window (default 60s), keyed by `client.address` | `autotel.security.anomaly` counter + `onSignal` callback — fired **once per crossing**, so alert volume stays bounded under attack |
309
+
310
+ Why this pairing matters: a credential-stuffing run at 10% baseline sampling is invisible in traces and a `/.env` probe is one boring 404 in your logs — but flagged spans **bypass tail sampling**, and the counters give security teams something to alert on. The interesting traffic is guaranteed to exist in your backend.
311
+
312
+ Design notes:
313
+
314
+ - Patterns are **conservative** (scanner/probe traffic, not a WAF) — `union+station+select` in a search query does not flag. Extend with `extraPatterns`.
315
+ - Burst tracking is **per-process** with bounded memory (`maxKeys`, default 10k clients, oldest evicted) — random-IP floods can't grow the map forever. For fleet-wide correlation, alert on the metrics in your backend instead.
316
+ - Both metric emission and your `onSignal` callback are guarded — they can never break the span pipeline.
317
+
211
318
  ## Integration with Observability Backends
212
319
 
213
320
  Audit attributes are standard OpenTelemetry span attributes and work with any OTLP-compatible backend (Datadog, New Relic, Jaeger, etc.).
package/dist/index.cjs CHANGED
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var autotel = require('autotel');
4
+ var crypto = require('crypto');
5
+ var securitySchema = require('autotel/security-schema');
4
6
 
5
7
  // src/index.ts
6
8
  function resolveContext(ctx) {
@@ -52,6 +54,364 @@ function toAttributeValue(value) {
52
54
  return "<serialization-failed>";
53
55
  }
54
56
  }
57
+ function lazyCounter(name, description) {
58
+ let counter;
59
+ return {
60
+ add(value, attributes) {
61
+ try {
62
+ counter ??= autotel.createCounter(name, { description });
63
+ counter.add(value, attributes);
64
+ } catch {
65
+ }
66
+ }
67
+ };
68
+ }
69
+
70
+ // src/security.ts
71
+ var FIELD_ATTRIBUTES = {
72
+ name: securitySchema.SECURITY_ATTR.event,
73
+ category: securitySchema.SECURITY_ATTR.category,
74
+ outcome: securitySchema.SECURITY_ATTR.outcome,
75
+ severity: securitySchema.SECURITY_ATTR.severity,
76
+ actorId: securitySchema.SECURITY_ATTR.actorId,
77
+ targetType: securitySchema.SECURITY_ATTR.targetType,
78
+ targetId: securitySchema.SECURITY_ATTR.targetId,
79
+ tenantId: securitySchema.SECURITY_ATTR.tenantId,
80
+ reason: securitySchema.SECURITY_ATTR.reason
81
+ };
82
+ function flattenSecurityAttributes(metadata) {
83
+ const attributes = {
84
+ [securitySchema.SECURITY_ATTR.marker]: true,
85
+ [securitySchema.SECURITY_ATTR.severity]: metadata.severity ?? "info"
86
+ };
87
+ const droppedKeys = [];
88
+ for (const [key, value] of Object.entries(metadata)) {
89
+ const standardAttribute = FIELD_ATTRIBUTES[key];
90
+ if (standardAttribute === void 0 && autotel.REDACTOR_PATTERNS.sensitiveKey.test(key)) {
91
+ droppedKeys.push(key);
92
+ continue;
93
+ }
94
+ const attr = toAttributeValue(value);
95
+ if (attr !== void 0) {
96
+ attributes[standardAttribute ?? `security.${key}`] = attr;
97
+ }
98
+ }
99
+ if (droppedKeys.length > 0) {
100
+ attributes[securitySchema.SECURITY_ATTR.droppedKeys] = droppedKeys;
101
+ }
102
+ return attributes;
103
+ }
104
+ var eventsCounter = lazyCounter(
105
+ securitySchema.SECURITY_METRICS.events,
106
+ "Security events by name, category, outcome, and severity"
107
+ );
108
+ function countSecurityEvent(metadata) {
109
+ eventsCounter.add(1, {
110
+ event: metadata.name,
111
+ category: metadata.category,
112
+ outcome: metadata.outcome,
113
+ severity: metadata.severity ?? "info"
114
+ });
115
+ }
116
+ function securityEvent(metadata, options = {}) {
117
+ const traceCtx = resolveContext(options.ctx);
118
+ if (options.forceKeep !== false) {
119
+ traceCtx.setAttribute(autotel.AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
120
+ traceCtx.setAttribute(autotel.AUTOTEL_SAMPLING_TAIL_KEEP, true);
121
+ traceCtx.setAttribute(securitySchema.SECURITY_ATTR.forceKeep, true);
122
+ }
123
+ traceCtx.setAttributes(flattenSecurityAttributes(metadata));
124
+ if (options.metrics !== false) {
125
+ countSecurityEvent(metadata);
126
+ }
127
+ const logger = options.logger ?? autotel.getRequestLogger();
128
+ logger.set({
129
+ security: {
130
+ name: metadata.name,
131
+ category: metadata.category,
132
+ outcome: metadata.outcome,
133
+ severity: metadata.severity ?? "info",
134
+ ...metadata.reason !== void 0 && { reason: metadata.reason },
135
+ forceKeep: options.forceKeep !== false
136
+ }
137
+ });
138
+ if (options.emitNow) {
139
+ logger.emitNow();
140
+ }
141
+ }
142
+ async function withSecurity(metadata, fn, options = {}) {
143
+ const traceCtx = resolveContext(options.ctx);
144
+ const logger = options.logger ?? autotel.getRequestLogger();
145
+ try {
146
+ const result = await fn(traceCtx, logger);
147
+ securityEvent(metadata, { ...options, ctx: traceCtx, logger });
148
+ return result;
149
+ } catch (error) {
150
+ const asError = error instanceof Error ? error : new Error(String(error));
151
+ securityEvent(
152
+ {
153
+ ...metadata,
154
+ outcome: "error",
155
+ // A failed security-sensitive operation is never less than an error,
156
+ // but an explicit `critical` stays critical.
157
+ severity: securitySchema.escalateSecuritySeverity(metadata.severity ?? "info", "error")
158
+ },
159
+ { ...options, ctx: traceCtx, logger }
160
+ );
161
+ logger.error(asError, {
162
+ security: {
163
+ name: metadata.name,
164
+ category: metadata.category
165
+ }
166
+ });
167
+ throw asError;
168
+ }
169
+ }
170
+ function hashIdentifier(value, options = {}) {
171
+ const length = options.length ?? 16;
172
+ return crypto.createHash("sha256").update(options.salt ? `${options.salt}:${value}` : value).digest("hex").slice(0, length);
173
+ }
174
+ var SUSPICIOUS_REQUEST_PATTERNS = {
175
+ path_traversal: /(\.\.[/\\]|%2e%2e(%2f|%5c|\/)|\.\.%2f|%252e%252e)/i,
176
+ sensitive_file_probe: /(\/\.env\b|\/\.git\b|\/etc\/passwd|\/wp-admin\b|\/\.aws\b|\/id_rsa\b)/i,
177
+ sqli_probe: /(\bunion\b[\s+%20]+(all[\s+%20]+)?select\b|'[\s+%20]*or[\s+%20]*'?1'?[\s+%20]*=[\s+%20]*'?1)/i,
178
+ xss_probe: /(<script\b|%3cscript)/i,
179
+ null_byte: /%00/
180
+ };
181
+ var TARGET_ATTRIBUTES = [
182
+ "url.path",
183
+ "url.full",
184
+ "http.target",
185
+ "http.url"
186
+ ];
187
+ function readAttribute(attributes, keys) {
188
+ for (const key of keys) {
189
+ const value = attributes[key];
190
+ if (value !== void 0) return value;
191
+ }
192
+ return void 0;
193
+ }
194
+ var SlidingWindow = class {
195
+ constructor(windowMs, maxKeys) {
196
+ this.windowMs = windowMs;
197
+ this.maxKeys = maxKeys;
198
+ }
199
+ windowMs;
200
+ maxKeys;
201
+ hits = /* @__PURE__ */ new Map();
202
+ /**
203
+ * Record a hit; returns the totals inside the window before and after it,
204
+ * so callers can signal exactly once on a threshold crossing.
205
+ */
206
+ record(key, now, weight = 1) {
207
+ let entries = this.hits.get(key);
208
+ if (!entries) {
209
+ if (this.hits.size >= this.maxKeys) {
210
+ const oldest = this.hits.keys().next().value;
211
+ if (oldest !== void 0) this.hits.delete(oldest);
212
+ }
213
+ entries = [];
214
+ this.hits.set(key, entries);
215
+ }
216
+ const cutoff = now - this.windowMs;
217
+ while (entries.length > 0 && entries[0][0] < cutoff) {
218
+ entries.shift();
219
+ }
220
+ let before = 0;
221
+ for (const [, w] of entries) before += w;
222
+ entries.push([now, weight]);
223
+ return { before, after: before + weight };
224
+ }
225
+ };
226
+ function resolveBurstConfig(option) {
227
+ if (option === false) return void 0;
228
+ const opts = option ?? {};
229
+ const windowMs = opts.windowMs ?? 6e4;
230
+ return {
231
+ statuses: new Set(opts.statuses ?? [401, 403]),
232
+ threshold: opts.threshold ?? 10,
233
+ windowMs,
234
+ keyAttribute: opts.keyAttribute ?? "client.address",
235
+ window: new SlidingWindow(windowMs, opts.maxKeys ?? 1e4)
236
+ };
237
+ }
238
+ function resolveLlmConfig(option) {
239
+ if (option === false) return void 0;
240
+ const opts = option ?? {};
241
+ const tokenBudget = opts.tokenBudget;
242
+ const windowMs = tokenBudget?.windowMs ?? 3e5;
243
+ return {
244
+ maxTokensPerCall: opts.maxTokensPerCall === false ? void 0 : opts.maxTokensPerCall ?? 1e5,
245
+ budget: tokenBudget && {
246
+ budget: tokenBudget.budget,
247
+ windowMs,
248
+ keyAttribute: tokenBudget.keyAttribute ?? "enduser.id",
249
+ window: new SlidingWindow(windowMs, tokenBudget.maxKeys ?? 1e4)
250
+ }
251
+ };
252
+ }
253
+ function createSecuritySignalProcessor(options = {}) {
254
+ const detect = options.detectSuspiciousRequests !== false;
255
+ const forceKeep = options.forceKeepSuspicious !== false;
256
+ const metricsEnabled = options.metrics !== false;
257
+ const deniedStatuses = new Set(
258
+ options.deniedStatuses ?? securitySchema.SECURITY_DENIED_STATUSES
259
+ );
260
+ const now = options.now ?? Date.now;
261
+ const patterns = {
262
+ ...SUSPICIOUS_REQUEST_PATTERNS,
263
+ ...options.extraPatterns
264
+ };
265
+ const burst = resolveBurstConfig(options.burst);
266
+ const llm = resolveLlmConfig(options.llm);
267
+ const counters = {
268
+ suspicious: lazyCounter(
269
+ securitySchema.SECURITY_METRICS.httpSuspicious,
270
+ "Requests matching suspicious-path patterns"
271
+ ),
272
+ denied: lazyCounter(
273
+ securitySchema.SECURITY_METRICS.httpDenied,
274
+ "HTTP responses with denied status codes (401/403/429)"
275
+ ),
276
+ anomaly: lazyCounter(
277
+ securitySchema.SECURITY_METRICS.anomaly,
278
+ "Security anomaly signals (e.g. auth-failure bursts)"
279
+ )
280
+ };
281
+ function count(which, attributes) {
282
+ if (!metricsEnabled) return;
283
+ counters[which].add(1, attributes);
284
+ }
285
+ function emit(signal) {
286
+ try {
287
+ options.onSignal?.(signal);
288
+ } catch {
289
+ }
290
+ }
291
+ function checkDeniedResponse(span) {
292
+ const status = readAttribute(span.attributes, securitySchema.HTTP_STATUS_ATTRIBUTES);
293
+ if (typeof status !== "number" || !deniedStatuses.has(status)) return;
294
+ count("denied", { status });
295
+ if (!burst || !burst.statuses.has(status)) return;
296
+ const key = readAttribute(span.attributes, [
297
+ burst.keyAttribute,
298
+ "http.client_ip"
299
+ ]);
300
+ if (typeof key !== "string" || key.length === 0) return;
301
+ const { before, after } = burst.window.record(key, now());
302
+ if (before < burst.threshold && after >= burst.threshold) {
303
+ count("anomaly", { signal: "auth_failure_burst", status });
304
+ emit({
305
+ signal: "auth_failure_burst",
306
+ key,
307
+ count: after,
308
+ windowMs: burst.windowMs,
309
+ status
310
+ });
311
+ }
312
+ }
313
+ function checkLlmConsumption(span) {
314
+ if (!llm) return;
315
+ const total = readAttribute(span.attributes, ["gen_ai.usage.total_tokens"]);
316
+ let tokens;
317
+ if (typeof total === "number") {
318
+ tokens = total;
319
+ } else {
320
+ const input = readAttribute(span.attributes, ["gen_ai.usage.input_tokens"]);
321
+ const output = readAttribute(span.attributes, [
322
+ "gen_ai.usage.output_tokens"
323
+ ]);
324
+ if (typeof input === "number" || typeof output === "number") {
325
+ tokens = (typeof input === "number" ? input : 0) + (typeof output === "number" ? output : 0);
326
+ }
327
+ }
328
+ if (tokens === void 0 || tokens <= 0) return;
329
+ if (llm.maxTokensPerCall !== void 0 && tokens > llm.maxTokensPerCall) {
330
+ const model = readAttribute(span.attributes, [
331
+ "gen_ai.response.model",
332
+ "gen_ai.request.model"
333
+ ]);
334
+ count("anomaly", { signal: "llm_excessive_tokens" });
335
+ emit({
336
+ signal: "llm_excessive_tokens",
337
+ tokens,
338
+ maxTokens: llm.maxTokensPerCall,
339
+ ...typeof model === "string" && { model }
340
+ });
341
+ }
342
+ const budget = llm.budget;
343
+ if (!budget) return;
344
+ const key = readAttribute(span.attributes, [
345
+ budget.keyAttribute,
346
+ "client.address"
347
+ ]);
348
+ if (typeof key !== "string" || key.length === 0) return;
349
+ const { before, after } = budget.window.record(key, now(), tokens);
350
+ if (before < budget.budget && after >= budget.budget) {
351
+ count("anomaly", { signal: "llm_token_budget_exceeded" });
352
+ emit({
353
+ signal: "llm_token_budget_exceeded",
354
+ key,
355
+ tokens: after,
356
+ budget: budget.budget,
357
+ windowMs: budget.windowMs
358
+ });
359
+ }
360
+ }
361
+ return {
362
+ onStart(span) {
363
+ if (!detect) return;
364
+ const target = readAttribute(span.attributes, TARGET_ATTRIBUTES);
365
+ if (typeof target !== "string" || target.length === 0) return;
366
+ for (const [name, pattern] of Object.entries(patterns)) {
367
+ if (!pattern.test(target)) continue;
368
+ span.setAttribute(securitySchema.SECURITY_ATTR.suspiciousRequest, true);
369
+ span.setAttribute(securitySchema.SECURITY_ATTR.signal, name);
370
+ if (forceKeep) {
371
+ span.setAttribute(autotel.AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
372
+ span.setAttribute(autotel.AUTOTEL_SAMPLING_TAIL_KEEP, true);
373
+ }
374
+ count("suspicious", { pattern: name });
375
+ emit({ signal: "suspicious_request", pattern: name, target });
376
+ return;
377
+ }
378
+ },
379
+ onEnd(span) {
380
+ checkDeniedResponse(span);
381
+ checkLlmConsumption(span);
382
+ },
383
+ shutdown() {
384
+ return Promise.resolve();
385
+ },
386
+ forceFlush() {
387
+ return Promise.resolve();
388
+ }
389
+ };
390
+ }
391
+ function startSecurityHeartbeat(options = {}) {
392
+ const intervalMs = options.intervalMs ?? 6e4;
393
+ const attributes = options.attributes ?? {};
394
+ const counter = lazyCounter(
395
+ securitySchema.SECURITY_METRICS.heartbeat,
396
+ "Security-telemetry liveness signal \u2014 alert on its absence"
397
+ );
398
+ function beat() {
399
+ counter.add(1, attributes);
400
+ }
401
+ beat();
402
+ const timer = setInterval(beat, intervalMs);
403
+ timer.unref?.();
404
+ let stopped = false;
405
+ return {
406
+ stop() {
407
+ if (stopped) return;
408
+ stopped = true;
409
+ clearInterval(timer);
410
+ }
411
+ };
412
+ }
413
+
414
+ // src/index.ts
55
415
  function flattenAuditAttributes(metadata) {
56
416
  const attributes = {
57
417
  "autotel.audit": true
@@ -112,8 +472,14 @@ async function withAudit(metadata, fn, options = {}) {
112
472
  }
113
473
  }
114
474
 
475
+ exports.SUSPICIOUS_REQUEST_PATTERNS = SUSPICIOUS_REQUEST_PATTERNS;
476
+ exports.createSecuritySignalProcessor = createSecuritySignalProcessor;
115
477
  exports.forceKeepAuditEvent = forceKeepAuditEvent;
478
+ exports.hashIdentifier = hashIdentifier;
479
+ exports.securityEvent = securityEvent;
116
480
  exports.setAuditAttributes = setAuditAttributes;
481
+ exports.startSecurityHeartbeat = startSecurityHeartbeat;
117
482
  exports.withAudit = withAudit;
483
+ exports.withSecurity = withSecurity;
118
484
  //# sourceMappingURL=index.cjs.map
119
485
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["getTraceContext","otelTrace","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP","getRequestLogger"],"mappings":"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","file":"index.cjs","sourcesContent":["import {\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n getRequestLogger,\n getTraceContext,\n otelTrace,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\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\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\nfunction resolveContext(ctx?: AuditContext): AuditContext {\n if (ctx) return ctx;\n\n const ids = getTraceContext();\n const span = otelTrace.getActiveSpan();\n if (ids && span) {\n return {\n traceId: ids.traceId,\n spanId: ids.spanId,\n correlationId: ids.correlationId,\n setAttribute: (key, value) => span.setAttribute(key, value),\n setAttributes: (attrs) => span.setAttributes(attrs),\n };\n }\n\n throw new Error(\n '[autotel-audit] No active trace context. Wrap your handler with trace() or pass options.ctx.',\n );\n}\n\nfunction 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\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 = resolveContext(ctx);\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 = resolveContext(ctx);\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 = resolveContext(options.ctx);\n\n if (options.forceKeep !== false) {\n forceKeepAuditEvent(traceCtx);\n }\n\n setAuditAttributes(metadata, traceCtx);\n\n const logger = options.logger ?? getRequestLogger();\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"]}
1
+ {"version":3,"sources":["../src/context.ts","../src/lazy-counter.ts","../src/security.ts","../src/security-signals.ts","../src/security-heartbeat.ts","../src/index.ts"],"names":["getTraceContext","otelTrace","createCounter","SECURITY_ATTR","REDACTOR_PATTERNS","SECURITY_METRICS","AUTOTEL_SAMPLING_TAIL_EVALUATED","AUTOTEL_SAMPLING_TAIL_KEEP","getRequestLogger","escalateSecuritySeverity","createHash","SECURITY_DENIED_STATUSES","HTTP_STATUS_ATTRIBUTES"],"mappings":";;;;;;;AAYO,SAAS,eAAe,GAAA,EAAkC;AAC/D,EAAA,IAAI,KAAK,OAAO,GAAA;AAEhB,EAAA,MAAM,MAAMA,uBAAA,EAAgB;AAC5B,EAAA,MAAM,IAAA,GAAOC,kBAAU,aAAA,EAAc;AACrC,EAAA,IAAI,OAAO,IAAA,EAAM;AACf,IAAA,OAAO;AAAA,MACL,SAAS,GAAA,CAAI,OAAA;AAAA,MACb,QAAQ,GAAA,CAAI,MAAA;AAAA,MACZ,eAAe,GAAA,CAAI,aAAA;AAAA,MACnB,cAAc,CAAC,GAAA,EAAK,UAAU,IAAA,CAAK,YAAA,CAAa,KAAK,KAAK,CAAA;AAAA,MAC1D,aAAA,EAAe,CAAC,KAAA,KAAU,IAAA,CAAK,cAAc,KAAK;AAAA,KACpD;AAAA,EACF;AAEA,EAAA,MAAM,IAAI,KAAA;AAAA,IACR;AAAA,GACF;AACF;AAEO,SAAS,iBACd,KAAA,EACyE;AACzE,EAAA,IACE,OAAO,UAAU,QAAA,IACjB,OAAO,UAAU,QAAA,IACjB,OAAO,UAAU,SAAA,EACjB;AACA,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,IAAA,IAAI,MAAM,KAAA,CAAM,CAAC,UAAU,OAAO,KAAA,KAAU,QAAQ,CAAA,EAAG;AACrD,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,IAAI,MAAM,KAAA,CAAM,CAAC,UAAU,OAAO,KAAA,KAAU,QAAQ,CAAA,EAAG;AACrD,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,IAAI,MAAM,KAAA,CAAM,CAAC,UAAU,OAAO,KAAA,KAAU,SAAS,CAAA,EAAG;AACtD,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,IAAI;AACF,MAAA,OAAO,IAAA,CAAK,UAAU,KAAK,CAAA;AAAA,IAC7B,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,wBAAA;AAAA,IACT;AAAA,EACF;AAEA,EAAA,IAAI,iBAAiB,IAAA,EAAM;AACzB,IAAA,OAAO,MAAM,WAAA,EAAY;AAAA,EAC3B;AAEA,EAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,KAAA,KAAU,MAAA,EAAW;AACzC,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,UAAU,KAAK,CAAA;AAAA,EAC7B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,wBAAA;AAAA,EACT;AACF;ACjEO,SAAS,WAAA,CAAY,MAAc,WAAA,EAAkC;AAC1E,EAAA,IAAI,OAAA;AACJ,EAAA,OAAO;AAAA,IACL,GAAA,CAAI,OAAO,UAAA,EAAY;AACrB,MAAA,IAAI;AACF,QAAA,OAAA,KAAYC,qBAAA,CAAc,IAAA,EAAM,EAAE,WAAA,EAAa,CAAA;AAC/C,QAAA,OAAA,CAAQ,GAAA,CAAI,OAAO,UAAU,CAAA;AAAA,MAC/B,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAAA,GACF;AACF;;;AC6FA,IAAM,gBAAA,GAA2C;AAAA,EAC/C,MAAMC,4BAAA,CAAc,KAAA;AAAA,EACpB,UAAUA,4BAAA,CAAc,QAAA;AAAA,EACxB,SAASA,4BAAA,CAAc,OAAA;AAAA,EACvB,UAAUA,4BAAA,CAAc,QAAA;AAAA,EACxB,SAASA,4BAAA,CAAc,OAAA;AAAA,EACvB,YAAYA,4BAAA,CAAc,UAAA;AAAA,EAC1B,UAAUA,4BAAA,CAAc,QAAA;AAAA,EACxB,UAAUA,4BAAA,CAAc,QAAA;AAAA,EACxB,QAAQA,4BAAA,CAAc;AACxB,CAAA;AAEA,SAAS,0BACP,QAAA,EAC6E;AAC7E,EAAA,MAAM,UAAA,GAGF;AAAA,IACF,CAACA,4BAAA,CAAc,MAAM,GAAG,IAAA;AAAA,IACxB,CAACA,4BAAA,CAAc,QAAQ,GAAG,SAAS,QAAA,IAAY;AAAA,GACjD;AAEA,EAAA,MAAM,cAAwB,EAAC;AAC/B,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,EAAG;AACnD,IAAA,MAAM,iBAAA,GAAoB,iBAAiB,GAAG,CAAA;AAI9C,IAAA,IACE,sBAAsB,MAAA,IACtBC,yBAAA,CAAkB,YAAA,CAAa,IAAA,CAAK,GAAG,CAAA,EACvC;AACA,MAAA,WAAA,CAAY,KAAK,GAAG,CAAA;AACpB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,iBAAiB,KAAK,CAAA;AACnC,IAAA,IAAI,SAAS,MAAA,EAAW;AACtB,MAAA,UAAA,CAAW,iBAAA,IAAqB,CAAA,SAAA,EAAY,GAAG,CAAA,CAAE,CAAA,GAAI,IAAA;AAAA,IACvD;AAAA,EACF;AAEA,EAAA,IAAI,WAAA,CAAY,SAAS,CAAA,EAAG;AAC1B,IAAA,UAAA,CAAWD,4BAAA,CAAc,WAAW,CAAA,GAAI,WAAA;AAAA,EAC1C;AAEA,EAAA,OAAO,UAAA;AACT;AAEA,IAAM,aAAA,GAAgB,WAAA;AAAA,EACpBE,+BAAA,CAAiB,MAAA;AAAA,EACjB;AACF,CAAA;AAEA,SAAS,mBAAmB,QAAA,EAAuC;AACjE,EAAA,aAAA,CAAc,IAAI,CAAA,EAAG;AAAA,IACnB,OAAO,QAAA,CAAS,IAAA;AAAA,IAChB,UAAU,QAAA,CAAS,QAAA;AAAA,IACnB,SAAS,QAAA,CAAS,OAAA;AAAA,IAClB,QAAA,EAAU,SAAS,QAAA,IAAY;AAAA,GAChC,CAAA;AACH;AAqBO,SAAS,aAAA,CACd,QAAA,EACA,OAAA,GAAgC,EAAC,EAC3B;AACN,EAAA,MAAM,QAAA,GAAW,cAAA,CAAe,OAAA,CAAQ,GAAG,CAAA;AAE3C,EAAA,IAAI,OAAA,CAAQ,cAAc,KAAA,EAAO;AAC/B,IAAA,QAAA,CAAS,YAAA,CAAaC,yCAAiC,IAAI,CAAA;AAC3D,IAAA,QAAA,CAAS,YAAA,CAAaC,oCAA4B,IAAI,CAAA;AACtD,IAAA,QAAA,CAAS,YAAA,CAAaJ,4BAAA,CAAc,SAAA,EAAW,IAAI,CAAA;AAAA,EACrD;AAEA,EAAA,QAAA,CAAS,aAAA,CAAc,yBAAA,CAA0B,QAAQ,CAAC,CAAA;AAE1D,EAAA,IAAI,OAAA,CAAQ,YAAY,KAAA,EAAO;AAC7B,IAAA,kBAAA,CAAmB,QAAQ,CAAA;AAAA,EAC7B;AAEA,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,MAAA,IAAUK,wBAAA,EAAiB;AAClD,EAAA,MAAA,CAAO,GAAA,CAAI;AAAA,IACT,QAAA,EAAU;AAAA,MACR,MAAM,QAAA,CAAS,IAAA;AAAA,MACf,UAAU,QAAA,CAAS,QAAA;AAAA,MACnB,SAAS,QAAA,CAAS,OAAA;AAAA,MAClB,QAAA,EAAU,SAAS,QAAA,IAAY,MAAA;AAAA,MAC/B,GAAI,QAAA,CAAS,MAAA,KAAW,UAAa,EAAE,MAAA,EAAQ,SAAS,MAAA,EAAO;AAAA,MAC/D,SAAA,EAAW,QAAQ,SAAA,KAAc;AAAA;AACnC,GACD,CAAA;AAED,EAAA,IAAI,QAAQ,OAAA,EAAS;AACnB,IAAA,MAAA,CAAO,OAAA,EAAQ;AAAA,EACjB;AACF;AAeA,eAAsB,YAAA,CACpB,QAAA,EACA,EAAA,EACA,OAAA,GAA+B,EAAC,EACpB;AACZ,EAAA,MAAM,QAAA,GAAW,cAAA,CAAe,OAAA,CAAQ,GAAG,CAAA;AAC3C,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,MAAA,IAAUA,wBAAA,EAAiB;AAElD,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,MAAM,EAAA,CAAG,QAAA,EAAU,MAAM,CAAA;AACxC,IAAA,aAAA,CAAc,UAAU,EAAE,GAAG,SAAS,GAAA,EAAK,QAAA,EAAU,QAAQ,CAAA;AAC7D,IAAA,OAAO,MAAA;AAAA,EACT,SAAS,KAAA,EAAO;AACd,IAAA,MAAM,OAAA,GAAU,iBAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,KAAA,CAAM,MAAA,CAAO,KAAK,CAAC,CAAA;AACxE,IAAA,aAAA;AAAA,MACE;AAAA,QACE,GAAG,QAAA;AAAA,QACH,OAAA,EAAS,OAAA;AAAA;AAAA;AAAA,QAGT,QAAA,EAAUC,uCAAA,CAAyB,QAAA,CAAS,QAAA,IAAY,QAAQ,OAAO;AAAA,OACzE;AAAA,MACA,EAAE,GAAG,OAAA,EAAS,GAAA,EAAK,UAAU,MAAA;AAAO,KACtC;AACA,IAAA,MAAA,CAAO,MAAM,OAAA,EAAS;AAAA,MACpB,QAAA,EAAU;AAAA,QACR,MAAM,QAAA,CAAS,IAAA;AAAA,QACf,UAAU,QAAA,CAAS;AAAA;AACrB,KACD,CAAA;AACD,IAAA,MAAM,OAAA;AAAA,EACR;AACF;AAeO,SAAS,cAAA,CACd,KAAA,EACA,OAAA,GAAiC,EAAC,EAC1B;AACR,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,EAAA;AACjC,EAAA,OAAOC,kBAAW,QAAQ,CAAA,CACvB,OAAO,OAAA,CAAQ,IAAA,GAAO,GAAG,OAAA,CAAQ,IAAI,IAAI,KAAK,CAAA,CAAA,GAAK,KAAK,CAAA,CACxD,MAAA,CAAO,KAAK,CAAA,CACZ,KAAA,CAAM,GAAG,MAAM,CAAA;AACpB;AC/HO,IAAM,2BAAA,GAAsD;AAAA,EACjE,cAAA,EAAgB,oDAAA;AAAA,EAChB,oBAAA,EACE,wEAAA;AAAA,EACF,UAAA,EACE,+FAAA;AAAA,EACF,SAAA,EAAW,wBAAA;AAAA,EACX,SAAA,EAAW;AACb;AAEA,IAAM,iBAAA,GAAoB;AAAA,EACxB,UAAA;AAAA,EACA,UAAA;AAAA,EACA,aAAA;AAAA,EACA;AACF,CAAA;AAEA,SAAS,aAAA,CACP,YACA,IAAA,EAC4B;AAC5B,EAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,IAAA,MAAM,KAAA,GAAQ,WAAW,GAAG,CAAA;AAC5B,IAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAAA,EAClC;AACA,EAAA,OAAO,MAAA;AACT;AAMA,IAAM,gBAAN,MAAoB;AAAA,EAGlB,WAAA,CACmB,UACA,OAAA,EACjB;AAFiB,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AACA,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAAA,EAChB;AAAA,EAFgB,QAAA;AAAA,EACA,OAAA;AAAA,EAJF,IAAA,uBAAW,GAAA,EAAqC;AAAA;AAAA;AAAA;AAAA;AAAA,EAWjE,MAAA,CAAO,GAAA,EAAa,GAAA,EAAa,MAAA,GAAS,CAAA,EAAsC;AAC9E,IAAA,IAAI,OAAA,GAAU,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA;AAC/B,IAAA,IAAI,CAAC,OAAA,EAAS;AAEZ,MAAA,IAAI,IAAA,CAAK,IAAA,CAAK,IAAA,IAAQ,IAAA,CAAK,OAAA,EAAS;AAClC,QAAA,MAAM,SAAS,IAAA,CAAK,IAAA,CAAK,IAAA,EAAK,CAAE,MAAK,CAAE,KAAA;AACvC,QAAA,IAAI,MAAA,KAAW,MAAA,EAAW,IAAA,CAAK,IAAA,CAAK,OAAO,MAAM,CAAA;AAAA,MACnD;AACA,MAAA,OAAA,GAAU,EAAC;AACX,MAAA,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,OAAO,CAAA;AAAA,IAC5B;AAEA,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,QAAA;AAC1B,IAAA,OAAO,OAAA,CAAQ,SAAS,CAAA,IAAM,OAAA,CAAQ,CAAC,CAAA,CAAuB,CAAC,IAAI,MAAA,EAAQ;AACzE,MAAA,OAAA,CAAQ,KAAA,EAAM;AAAA,IAChB;AAEA,IAAA,IAAI,MAAA,GAAS,CAAA;AACb,IAAA,KAAA,MAAW,GAAG,CAAC,CAAA,IAAK,SAAS,MAAA,IAAU,CAAA;AACvC,IAAA,OAAA,CAAQ,IAAA,CAAK,CAAC,GAAA,EAAK,MAAM,CAAC,CAAA;AAC1B,IAAA,OAAO,EAAE,MAAA,EAAQ,KAAA,EAAO,MAAA,GAAS,MAAA,EAAO;AAAA,EAC1C;AACF,CAAA;AAWA,SAAS,mBACP,MAAA,EACyB;AACzB,EAAA,IAAI,MAAA,KAAW,OAAO,OAAO,MAAA;AAC7B,EAAA,MAAM,IAAA,GAAO,UAAU,EAAC;AACxB,EAAA,MAAM,QAAA,GAAW,KAAK,QAAA,IAAY,GAAA;AAClC,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,IAAI,GAAA,CAAI,IAAA,CAAK,YAAY,CAAC,GAAA,EAAK,GAAG,CAAC,CAAA;AAAA,IAC7C,SAAA,EAAW,KAAK,SAAA,IAAa,EAAA;AAAA,IAC7B,QAAA;AAAA,IACA,YAAA,EAAc,KAAK,YAAA,IAAgB,gBAAA;AAAA,IACnC,QAAQ,IAAI,aAAA,CAAc,QAAA,EAAU,IAAA,CAAK,WAAW,GAAM;AAAA,GAC5D;AACF;AAaA,SAAS,iBACP,MAAA,EACuB;AACvB,EAAA,IAAI,MAAA,KAAW,OAAO,OAAO,MAAA;AAC7B,EAAA,MAAM,IAAA,GAAO,UAAU,EAAC;AACxB,EAAA,MAAM,cAAc,IAAA,CAAK,WAAA;AACzB,EAAA,MAAM,QAAA,GAAW,aAAa,QAAA,IAAY,GAAA;AAC1C,EAAA,OAAO;AAAA,IACL,kBACE,IAAA,CAAK,gBAAA,KAAqB,KAAA,GACtB,MAAA,GACC,KAAK,gBAAA,IAAoB,GAAA;AAAA,IAChC,QAAQ,WAAA,IAAe;AAAA,MACrB,QAAQ,WAAA,CAAY,MAAA;AAAA,MACpB,QAAA;AAAA,MACA,YAAA,EAAc,YAAY,YAAA,IAAgB,YAAA;AAAA,MAC1C,QAAQ,IAAI,aAAA,CAAc,QAAA,EAAU,WAAA,CAAY,WAAW,GAAM;AAAA;AACnE,GACF;AACF;AAEO,SAAS,6BAAA,CACd,OAAA,GAA0C,EAAC,EAClB;AACzB,EAAA,MAAM,MAAA,GAAS,QAAQ,wBAAA,KAA6B,KAAA;AACpD,EAAA,MAAM,SAAA,GAAY,QAAQ,mBAAA,KAAwB,KAAA;AAClD,EAAA,MAAM,cAAA,GAAiB,QAAQ,OAAA,KAAY,KAAA;AAC3C,EAAA,MAAM,iBAAiB,IAAI,GAAA;AAAA,IACzB,QAAQ,cAAA,IAAkBC;AAAA,GAC5B;AACA,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,IAAO,IAAA,CAAK,GAAA;AAEhC,EAAA,MAAM,QAAA,GAAmC;AAAA,IACvC,GAAG,2BAAA;AAAA,IACH,GAAG,OAAA,CAAQ;AAAA,GACb;AAEA,EAAA,MAAM,KAAA,GAAQ,kBAAA,CAAmB,OAAA,CAAQ,KAAK,CAAA;AAC9C,EAAA,MAAM,GAAA,GAAM,gBAAA,CAAiB,OAAA,CAAQ,GAAG,CAAA;AAExC,EAAA,MAAM,QAAA,GAAW;AAAA,IACf,UAAA,EAAY,WAAA;AAAA,MACVN,+BAAAA,CAAiB,cAAA;AAAA,MACjB;AAAA,KACF;AAAA,IACA,MAAA,EAAQ,WAAA;AAAA,MACNA,+BAAAA,CAAiB,UAAA;AAAA,MACjB;AAAA,KACF;AAAA,IACA,OAAA,EAAS,WAAA;AAAA,MACPA,+BAAAA,CAAiB,OAAA;AAAA,MACjB;AAAA;AACF,GACF;AAEA,EAAA,SAAS,KAAA,CACP,OACA,UAAA,EACM;AACN,IAAA,IAAI,CAAC,cAAA,EAAgB;AACrB,IAAA,QAAA,CAAS,KAAK,CAAA,CAAE,GAAA,CAAI,CAAA,EAAG,UAAU,CAAA;AAAA,EACnC;AAEA,EAAA,SAAS,KAAK,MAAA,EAA8B;AAC1C,IAAA,IAAI;AACF,MAAA,OAAA,CAAQ,WAAW,MAAM,CAAA;AAAA,IAC3B,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AAEA,EAAA,SAAS,oBAAoB,IAAA,EAA8B;AACzD,IAAA,MAAM,MAAA,GAAS,aAAA,CAAc,IAAA,CAAK,UAAA,EAAYO,qCAAsB,CAAA;AACpE,IAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,CAAC,cAAA,CAAe,GAAA,CAAI,MAAM,CAAA,EAAG;AAE/D,IAAA,KAAA,CAAM,QAAA,EAAU,EAAE,MAAA,EAAQ,CAAA;AAE1B,IAAA,IAAI,CAAC,KAAA,IAAS,CAAC,MAAM,QAAA,CAAS,GAAA,CAAI,MAAM,CAAA,EAAG;AAE3C,IAAA,MAAM,GAAA,GAAM,aAAA,CAAc,IAAA,CAAK,UAAA,EAAY;AAAA,MACzC,KAAA,CAAM,YAAA;AAAA,MACN;AAAA,KACD,CAAA;AACD,IAAA,IAAI,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,CAAI,WAAW,CAAA,EAAG;AAEjD,IAAA,MAAM,EAAE,QAAQ,KAAA,EAAM,GAAI,MAAM,MAAA,CAAO,MAAA,CAAO,GAAA,EAAK,GAAA,EAAK,CAAA;AAGxD,IAAA,IAAI,MAAA,GAAS,KAAA,CAAM,SAAA,IAAa,KAAA,IAAS,MAAM,SAAA,EAAW;AACxD,MAAA,KAAA,CAAM,SAAA,EAAW,EAAE,MAAA,EAAQ,oBAAA,EAAsB,QAAQ,CAAA;AACzD,MAAA,IAAA,CAAK;AAAA,QACH,MAAA,EAAQ,oBAAA;AAAA,QACR,GAAA;AAAA,QACA,KAAA,EAAO,KAAA;AAAA,QACP,UAAU,KAAA,CAAM,QAAA;AAAA,QAChB;AAAA,OACD,CAAA;AAAA,IACH;AAAA,EACF;AAEA,EAAA,SAAS,oBAAoB,IAAA,EAA8B;AACzD,IAAA,IAAI,CAAC,GAAA,EAAK;AAEV,IAAA,MAAM,QAAQ,aAAA,CAAc,IAAA,CAAK,UAAA,EAAY,CAAC,2BAA2B,CAAC,CAAA;AAC1E,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,MAAA,MAAA,GAAS,KAAA;AAAA,IACX,CAAA,MAAO;AACL,MAAA,MAAM,QAAQ,aAAA,CAAc,IAAA,CAAK,UAAA,EAAY,CAAC,2BAA2B,CAAC,CAAA;AAC1E,MAAA,MAAM,MAAA,GAAS,aAAA,CAAc,IAAA,CAAK,UAAA,EAAY;AAAA,QAC5C;AAAA,OACD,CAAA;AACD,MAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,OAAO,WAAW,QAAA,EAAU;AAC3D,QAAA,MAAA,GAAA,CACG,OAAO,UAAU,QAAA,GAAW,KAAA,GAAQ,MACpC,OAAO,MAAA,KAAW,WAAW,MAAA,GAAS,CAAA,CAAA;AAAA,MAC3C;AAAA,IACF;AACA,IAAA,IAAI,MAAA,KAAW,MAAA,IAAa,MAAA,IAAU,CAAA,EAAG;AAEzC,IAAA,IAAI,GAAA,CAAI,gBAAA,KAAqB,MAAA,IAAa,MAAA,GAAS,IAAI,gBAAA,EAAkB;AACvE,MAAA,MAAM,KAAA,GAAQ,aAAA,CAAc,IAAA,CAAK,UAAA,EAAY;AAAA,QAC3C,uBAAA;AAAA,QACA;AAAA,OACD,CAAA;AACD,MAAA,KAAA,CAAM,SAAA,EAAW,EAAE,MAAA,EAAQ,sBAAA,EAAwB,CAAA;AACnD,MAAA,IAAA,CAAK;AAAA,QACH,MAAA,EAAQ,sBAAA;AAAA,QACR,MAAA;AAAA,QACA,WAAW,GAAA,CAAI,gBAAA;AAAA,QACf,GAAI,OAAO,KAAA,KAAU,QAAA,IAAY,EAAE,KAAA;AAAM,OAC1C,CAAA;AAAA,IACH;AAEA,IAAA,MAAM,SAAS,GAAA,CAAI,MAAA;AACnB,IAAA,IAAI,CAAC,MAAA,EAAQ;AAEb,IAAA,MAAM,GAAA,GAAM,aAAA,CAAc,IAAA,CAAK,UAAA,EAAY;AAAA,MACzC,MAAA,CAAO,YAAA;AAAA,MACP;AAAA,KACD,CAAA;AACD,IAAA,IAAI,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,CAAI,WAAW,CAAA,EAAG;AAEjD,IAAA,MAAM,EAAE,MAAA,EAAQ,KAAA,EAAM,GAAI,MAAA,CAAO,OAAO,MAAA,CAAO,GAAA,EAAK,GAAA,EAAI,EAAG,MAAM,CAAA;AACjE,IAAA,IAAI,MAAA,GAAS,MAAA,CAAO,MAAA,IAAU,KAAA,IAAS,OAAO,MAAA,EAAQ;AACpD,MAAA,KAAA,CAAM,SAAA,EAAW,EAAE,MAAA,EAAQ,2BAAA,EAA6B,CAAA;AACxD,MAAA,IAAA,CAAK;AAAA,QACH,MAAA,EAAQ,2BAAA;AAAA,QACR,GAAA;AAAA,QACA,MAAA,EAAQ,KAAA;AAAA,QACR,QAAQ,MAAA,CAAO,MAAA;AAAA,QACf,UAAU,MAAA,CAAO;AAAA,OAClB,CAAA;AAAA,IACH;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,QAAQ,IAAA,EAAM;AACZ,MAAA,IAAI,CAAC,MAAA,EAAQ;AAEb,MAAA,MAAM,MAAA,GAAS,aAAA,CAAc,IAAA,CAAK,UAAA,EAAY,iBAAiB,CAAA;AAC/D,MAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,CAAO,WAAW,CAAA,EAAG;AAEvD,MAAA,KAAA,MAAW,CAAC,IAAA,EAAM,OAAO,KAAK,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,EAAG;AACtD,QAAA,IAAI,CAAC,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAA,EAAG;AAE3B,QAAA,IAAA,CAAK,YAAA,CAAaT,4BAAAA,CAAc,iBAAA,EAAmB,IAAI,CAAA;AACvD,QAAA,IAAA,CAAK,YAAA,CAAaA,4BAAAA,CAAc,MAAA,EAAQ,IAAI,CAAA;AAC5C,QAAA,IAAI,SAAA,EAAW;AACb,UAAA,IAAA,CAAK,YAAA,CAAaG,yCAAiC,IAAI,CAAA;AACvD,UAAA,IAAA,CAAK,YAAA,CAAaC,oCAA4B,IAAI,CAAA;AAAA,QACpD;AAEA,QAAA,KAAA,CAAM,YAAA,EAAc,EAAE,OAAA,EAAS,IAAA,EAAM,CAAA;AACrC,QAAA,IAAA,CAAK,EAAE,MAAA,EAAQ,oBAAA,EAAsB,OAAA,EAAS,IAAA,EAAM,QAAQ,CAAA;AAC5D,QAAA;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IAEA,MAAM,IAAA,EAAM;AACV,MAAA,mBAAA,CAAoB,IAAI,CAAA;AACxB,MAAA,mBAAA,CAAoB,IAAI,CAAA;AAAA,IAC1B,CAAA;AAAA,IAEA,QAAA,GAAW;AACT,MAAA,OAAO,QAAQ,OAAA,EAAQ;AAAA,IACzB,CAAA;AAAA,IAEA,UAAA,GAAa;AACX,MAAA,OAAO,QAAQ,OAAA,EAAQ;AAAA,IACzB;AAAA,GACF;AACF;ACrbO,SAAS,sBAAA,CACd,OAAA,GAAoC,EAAC,EAClB;AACnB,EAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,GAAA;AACzC,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,UAAA,IAAc,EAAC;AAE1C,EAAA,MAAM,OAAA,GAAU,WAAA;AAAA,IACdF,+BAAAA,CAAiB,SAAA;AAAA,IACjB;AAAA,GACF;AAEA,EAAA,SAAS,IAAA,GAAa;AACpB,IAAA,OAAA,CAAQ,GAAA,CAAI,GAAG,UAAU,CAAA;AAAA,EAC3B;AAEA,EAAA,IAAA,EAAK;AACL,EAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,IAAA,EAAM,UAAU,CAAA;AAE1C,EAAA,KAAA,CAAM,KAAA,IAAQ;AAEd,EAAA,IAAI,OAAA,GAAU,KAAA;AACd,EAAA,OAAO;AAAA,IACL,IAAA,GAAO;AACL,MAAA,IAAI,OAAA,EAAS;AACb,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,aAAA,CAAc,KAAK,CAAA;AAAA,IACrB;AAAA,GACF;AACF;;;ACjCA,SAAS,uBACP,QAAA,EAC6E;AAC7E,EAAA,MAAM,UAAA,GAGF;AAAA,IACF,eAAA,EAAiB;AAAA,GACnB;AAEA,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,EAAG;AACnD,IAAA,MAAM,IAAA,GAAO,iBAAiB,KAAK,CAAA;AACnC,IAAA,IAAI,SAAS,MAAA,EAAW;AACtB,MAAA,UAAA,CAAW,CAAA,MAAA,EAAS,GAAG,CAAA,CAAE,CAAA,GAAI,IAAA;AAAA,IAC/B;AAAA,EACF;AAEA,EAAA,OAAO,UAAA;AACT;AAEO,SAAS,oBAAoB,GAAA,EAA0B;AAC5D,EAAA,MAAM,QAAA,GAAW,eAAe,GAAG,CAAA;AACnC,EAAA,QAAA,CAAS,YAAA,CAAaC,yCAAiC,IAAI,CAAA;AAC3D,EAAA,QAAA,CAAS,YAAA,CAAaC,oCAA4B,IAAI,CAAA;AACtD,EAAA,QAAA,CAAS,YAAA,CAAa,4BAA4B,IAAI,CAAA;AACxD;AAEO,SAAS,kBAAA,CACd,UACA,GAAA,EACM;AACN,EAAA,MAAM,QAAA,GAAW,eAAe,GAAG,CAAA;AACnC,EAAA,QAAA,CAAS,aAAA,CAAc,sBAAA,CAAuB,QAAQ,CAAC,CAAA;AACzD;AAEA,eAAsB,SAAA,CACpB,QAAA,EACA,EAAA,EACA,OAAA,GAA4B,EAAC,EACjB;AACZ,EAAA,MAAM,QAAA,GAAW,cAAA,CAAe,OAAA,CAAQ,GAAG,CAAA;AAE3C,EAAA,IAAI,OAAA,CAAQ,cAAc,KAAA,EAAO;AAC/B,IAAA,mBAAA,CAAoB,QAAQ,CAAA;AAAA,EAC9B;AAEA,EAAA,kBAAA,CAAmB,UAAU,QAAQ,CAAA;AAErC,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,MAAA,IAAUC,wBAAAA,EAAiB;AAClD,EAAA,MAAA,CAAO,GAAA,CAAI;AAAA,IACT,KAAA,EAAO;AAAA,MACL,GAAG,QAAA;AAAA,MACH,SAAA,EAAW,QAAQ,SAAA,KAAc;AAAA;AACnC,GACD,CAAA;AAED,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,MAAM,EAAA,CAAG,QAAA,EAAU,MAAM,CAAA;AAExC,IAAA,IAAI,CAAC,SAAS,OAAA,EAAS;AACrB,MAAA,kBAAA,CAAmB,EAAE,GAAG,QAAA,EAAU,OAAA,EAAS,SAAA,IAAa,QAAQ,CAAA;AAAA,IAClE;AAEA,IAAA,IAAI,QAAQ,OAAA,EAAS;AACnB,MAAA,MAAA,CAAO,OAAA,EAAQ;AAAA,IACjB;AAEA,IAAA,OAAO,MAAA;AAAA,EACT,SAAS,KAAA,EAAO;AACd,IAAA,MAAM,OAAA,GAAU,iBAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,KAAA,CAAM,MAAA,CAAO,KAAK,CAAC,CAAA;AACxE,IAAA,kBAAA,CAAmB,EAAE,GAAG,QAAA,EAAU,OAAA,EAAS,SAAA,IAAa,QAAQ,CAAA;AAChE,IAAA,MAAA,CAAO,MAAM,OAAA,EAAS;AAAA,MACpB,KAAA,EAAO;AAAA,QACL,QAAQ,QAAA,CAAS,MAAA;AAAA,QACjB,UAAU,QAAA,CAAS;AAAA;AACrB,KACD,CAAA;AAED,IAAA,IAAI,QAAQ,OAAA,EAAS;AACnB,MAAA,MAAA,CAAO,OAAA,EAAQ;AAAA,IACjB;AAEA,IAAA,MAAM,OAAA;AAAA,EACR;AACF","file":"index.cjs","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\nexport function resolveContext(ctx?: AuditContext): AuditContext {\n if (ctx) return ctx;\n\n const ids = getTraceContext();\n const span = otelTrace.getActiveSpan();\n if (ids && span) {\n return {\n traceId: ids.traceId,\n spanId: ids.spanId,\n correlationId: ids.correlationId,\n setAttribute: (key, value) => span.setAttribute(key, value),\n setAttributes: (attrs) => span.setAttributes(attrs),\n };\n }\n\n throw new Error(\n '[autotel-audit] No active trace context. Wrap your handler with trace() or pass options.ctx.',\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 getRequestLogger,\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 { resolveContext, toAttributeValue, type AuditContext } 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\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 = resolveContext(options.ctx);\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 if (options.metrics !== false) {\n countSecurityEvent(metadata);\n }\n\n const logger = options.logger ?? getRequestLogger();\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 = resolveContext(options.ctx);\n const logger = options.logger ?? getRequestLogger();\n\n try {\n const result = await fn(traceCtx, logger);\n securityEvent(metadata, { ...options, ctx: traceCtx, 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, 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 getRequestLogger,\n} from 'autotel';\nimport type { RequestLogger } from 'autotel';\nimport { resolveContext, toAttributeValue, type AuditContext } from './context';\n\nexport type { AuditContext } 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\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 = resolveContext(ctx);\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 = resolveContext(ctx);\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 = resolveContext(options.ctx);\n\n if (options.forceKeep !== false) {\n forceKeepAuditEvent(traceCtx);\n }\n\n setAuditAttributes(metadata, traceCtx);\n\n const logger = options.logger ?? getRequestLogger();\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"]}