autotel-audit 0.2.1 → 0.3.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/src/security.ts CHANGED
@@ -3,7 +3,8 @@ import {
3
3
  AUTOTEL_SAMPLING_TAIL_EVALUATED,
4
4
  AUTOTEL_SAMPLING_TAIL_KEEP,
5
5
  REDACTOR_PATTERNS,
6
- getRequestLogger,
6
+ createNoopRequestLogger,
7
+ getRequestLoggerSafe,
7
8
  } from 'autotel';
8
9
  import type { RequestLogger } from 'autotel';
9
10
  import {
@@ -12,7 +13,15 @@ import {
12
13
  escalateSecuritySeverity,
13
14
  } from 'autotel/security-schema';
14
15
  import type { SecuritySeverity } from 'autotel/security-schema';
15
- import { resolveContext, toAttributeValue, type AuditContext } from './context';
16
+ import {
17
+ MISSING_CONTEXT_MESSAGE,
18
+ noopAuditContext,
19
+ resolveContextSafe,
20
+ toAttributeValue,
21
+ warnMissingContextOnce,
22
+ type AuditContext,
23
+ type OnMissingContext,
24
+ } from './context';
16
25
  import { lazyCounter } from './lazy-counter';
17
26
 
18
27
  export type { SecuritySeverity };
@@ -105,6 +114,12 @@ export interface SecurityEventOptions {
105
114
  * to a stable catalogue, never interpolate user input into them.
106
115
  */
107
116
  metrics?: boolean;
117
+ /**
118
+ * Behaviour when no trace context can be resolved. Defaults to `warn`
119
+ * (best-effort: record nothing, warn once). A dropped security event is still
120
+ * better than a crashed request — but the warning makes the gap visible.
121
+ */
122
+ onMissingContext?: OnMissingContext;
108
123
  }
109
124
 
110
125
  export type WithSecurityOptions = SecurityEventOptions;
@@ -201,7 +216,24 @@ export function securityEvent(
201
216
  metadata: SecurityEventMetadata,
202
217
  options: SecurityEventOptions = {},
203
218
  ): void {
204
- const traceCtx = resolveContext(options.ctx);
219
+ const traceCtx = resolveContextSafe(options.ctx);
220
+
221
+ // Counters are independent of trace context — always record the security signal
222
+ // even when there's no span to attach attributes to.
223
+ if (options.metrics !== false) {
224
+ countSecurityEvent(metadata);
225
+ }
226
+
227
+ if (!traceCtx) {
228
+ const mode = options.onMissingContext ?? 'warn';
229
+ if (mode === 'throw') {
230
+ throw new Error(MISSING_CONTEXT_MESSAGE);
231
+ }
232
+ if (mode === 'warn') {
233
+ warnMissingContextOnce(metadata.name);
234
+ }
235
+ return;
236
+ }
205
237
 
206
238
  if (options.forceKeep !== false) {
207
239
  traceCtx.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
@@ -211,11 +243,7 @@ export function securityEvent(
211
243
 
212
244
  traceCtx.setAttributes(flattenSecurityAttributes(metadata));
213
245
 
214
- if (options.metrics !== false) {
215
- countSecurityEvent(metadata);
216
- }
217
-
218
- const logger = options.logger ?? getRequestLogger();
246
+ const logger = options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();
219
247
  logger.set({
220
248
  security: {
221
249
  name: metadata.name,
@@ -250,12 +278,14 @@ export async function withSecurity<T>(
250
278
  fn: (ctx: AuditContext, logger: RequestLogger) => T | Promise<T>,
251
279
  options: WithSecurityOptions = {},
252
280
  ): Promise<T> {
253
- const traceCtx = resolveContext(options.ctx);
254
- const logger = options.logger ?? getRequestLogger();
281
+ const traceCtx = resolveContextSafe(options.ctx);
282
+ const logger =
283
+ options.logger ?? getRequestLoggerSafe() ?? createNoopRequestLogger();
284
+ const ctx = traceCtx ?? noopAuditContext();
255
285
 
256
286
  try {
257
- const result = await fn(traceCtx, logger);
258
- securityEvent(metadata, { ...options, ctx: traceCtx, logger });
287
+ const result = await fn(ctx, logger);
288
+ securityEvent(metadata, { ...options, ctx: traceCtx ?? undefined, logger });
259
289
  return result;
260
290
  } catch (error) {
261
291
  const asError = error instanceof Error ? error : new Error(String(error));
@@ -267,7 +297,7 @@ export async function withSecurity<T>(
267
297
  // but an explicit `critical` stays critical.
268
298
  severity: escalateSecuritySeverity(metadata.severity ?? 'info', 'error'),
269
299
  },
270
- { ...options, ctx: traceCtx, logger },
300
+ { ...options, ctx: traceCtx ?? undefined, logger },
271
301
  );
272
302
  logger.error(asError, {
273
303
  security: {