{"version":3,"file":"security.cjs","names":["EventSubscriber","createHttpClient","SECURITY_SEVERITY_RANK","postJsonWithRetry"],"sources":["../src/security.ts"],"sourcesContent":["/**\n * SecuritySubscriber - Forward security events to a webhook, SIEM, or pager\n *\n * Pairs with `autotel-audit`'s security-event schema: track security events\n * through the Events API and this subscriber forwards the ones that matter\n * (severity-gated) to your alerting destination.\n *\n * @example Webhook (SIEM / incident channel)\n * ```typescript\n * import { Events } from 'autotel/events';\n * import { SecuritySubscriber } from 'autotel-subscribers/security';\n *\n * const events = new Events('api', {\n *   subscribers: [\n *     new SecuritySubscriber({\n *       webhookUrl: process.env.SECURITY_WEBHOOK_URL!,\n *       minSeverity: 'error',\n *     }),\n *   ],\n * });\n *\n * events.trackEvent('security.access.tenant.violation', {\n *   category: 'authorization',\n *   outcome: 'denied',\n *   severity: 'critical',\n *   actorId: 'user-1',\n * });\n * ```\n *\n * @example Custom handler (PagerDuty, OpsGenie, internal bus)\n * ```typescript\n * new SecuritySubscriber({\n *   handler: async (alert) => pagerduty.trigger(alert),\n *   minSeverity: 'critical',\n * });\n * ```\n */\n\nimport {\n  EventSubscriber,\n  type EventPayload,\n  type AutotelEventContext,\n} from './event-subscriber-base';\nimport type { EventAttributes } from 'autotel/event-subscriber';\nimport {\n  SECURITY_SEVERITY_RANK,\n  parseSecuritySeverity,\n} from 'autotel/security-schema';\nimport type { SecuritySeverity } from 'autotel/security-schema';\nimport { createHttpClient } from './http-client';\nimport { postJsonWithRetry } from './webhook-delivery';\n\nexport type SecurityAlertSeverity = SecuritySeverity;\n\n/** Normalized alert passed to handlers and POSTed to webhooks. */\nexport interface SecurityAlert {\n  /** Full event name, e.g. `security.auth.login.failed`. */\n  event: string;\n  severity: SecurityAlertSeverity;\n  category?: string;\n  outcome?: string;\n  reason?: string;\n  /** Remaining event attributes (severity/category/outcome/reason lifted out). */\n  attributes?: EventAttributes;\n  /** ISO 8601. */\n  timestamp: string;\n  /** Trace correlation, when the Events pipeline includes it. */\n  trace?: AutotelEventContext;\n}\n\nexport interface SecuritySubscriberConfig {\n  /** POST alerts as JSON to this URL. Required unless `handler` is set. */\n  webhookUrl?: string;\n  /** Extra headers for the webhook request (e.g. auth). */\n  headers?: Record<string, string>;\n  /** Custom destination — takes precedence over `webhookUrl`. */\n  handler?: (alert: SecurityAlert) => void | Promise<void>;\n  /** Forward events at or above this severity. Default `warning`. */\n  minSeverity?: SecurityAlertSeverity;\n  /**\n   * Events are recognized as security events when their name starts with\n   * this prefix. Default `security.`.\n   */\n  eventPrefix?: string;\n  /** Extra predicate applied after the prefix/severity gates. */\n  filter?: (payload: EventPayload) => boolean;\n  /** Enable/disable subscriber. Default true. */\n  enabled?: boolean;\n  /** Webhook delivery attempts including the first. Default 3. */\n  maxRetries?: number;\n  /** Webhook request timeout in milliseconds. Default 30_000. */\n  timeoutMs?: number;\n  /** Base webhook retry backoff; doubles per attempt. Default 1000. */\n  retryDelayMs?: number;\n}\n\nexport class SecuritySubscriber extends EventSubscriber {\n  readonly name = 'SecuritySubscriber';\n  readonly version = '1.0.0';\n\n  private config: {\n    webhookUrl?: string;\n    headers: Record<string, string>;\n    handler?: (alert: SecurityAlert) => void | Promise<void>;\n    minSeverity: SecurityAlertSeverity;\n    eventPrefix: string;\n    filter?: (payload: EventPayload) => boolean;\n    maxRetries?: number;\n    retryDelayMs?: number;\n  };\n\n  private readonly httpClient;\n\n  constructor(config: SecuritySubscriberConfig) {\n    super();\n\n    this.config = {\n      webhookUrl: config.webhookUrl,\n      headers: config.headers ?? {},\n      handler: config.handler,\n      minSeverity: config.minSeverity ?? 'warning',\n      eventPrefix: config.eventPrefix ?? 'security.',\n      filter: config.filter,\n      maxRetries: config.maxRetries,\n      retryDelayMs: config.retryDelayMs,\n    };\n    this.httpClient = createHttpClient({ timeoutMs: config.timeoutMs });\n\n    this.enabled = config.enabled ?? true;\n\n    if (!this.config.webhookUrl && !this.config.handler) {\n      console.error(\n        '[SecuritySubscriber] No webhookUrl or handler provided - subscriber disabled',\n      );\n      this.enabled = false;\n    }\n  }\n\n  protected async sendToDestination(payload: EventPayload): Promise<void> {\n    if (!payload.name.startsWith(this.config.eventPrefix)) {\n      return; // Not a security event\n    }\n\n    const severity = parseSecuritySeverity(payload.attributes?.severity);\n    if (\n      SECURITY_SEVERITY_RANK[severity] <\n      SECURITY_SEVERITY_RANK[this.config.minSeverity]\n    ) {\n      return; // Below the alerting bar\n    }\n\n    const filterFn = this.config.filter;\n    if (filterFn && !filterFn(payload)) {\n      return;\n    }\n\n    const alert = this.toAlert(payload, severity);\n\n    if (this.config.handler) {\n      await this.config.handler(alert);\n      return;\n    }\n\n    await postJsonWithRetry(\n      this.httpClient,\n      this.config.webhookUrl as string,\n      alert,\n      {\n        headers: this.config.headers,\n        maxRetries: this.config.maxRetries,\n        retryDelayMs: this.config.retryDelayMs,\n        label: 'Security webhook',\n      },\n    );\n  }\n\n  private toAlert(\n    payload: EventPayload,\n    severity: SecurityAlertSeverity,\n  ): SecurityAlert {\n    const {\n      severity: _severity,\n      category,\n      outcome,\n      reason,\n      ...rest\n    } = payload.attributes ?? {};\n\n    return {\n      event: payload.name,\n      severity,\n      ...(typeof category === 'string' && { category }),\n      ...(typeof outcome === 'string' && { outcome }),\n      ...(typeof reason === 'string' && { reason }),\n      ...(Object.keys(rest).length > 0 && { attributes: rest }),\n      timestamp: payload.timestamp,\n      ...(payload.autotel && { trace: payload.autotel }),\n    };\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgGA,IAAa,qBAAb,cAAwCA,8CAAgB;CACtD,AAAS,OAAO;CAChB,AAAS,UAAU;CAEnB,AAAQ;CAWR,AAAiB;CAEjB,YAAY,QAAkC;EAC5C,MAAM;EAEN,KAAK,SAAS;GACZ,YAAY,OAAO;GACnB,SAAS,OAAO,WAAW,CAAC;GAC5B,SAAS,OAAO;GAChB,aAAa,OAAO,eAAe;GACnC,aAAa,OAAO,eAAe;GACnC,QAAQ,OAAO;GACf,YAAY,OAAO;GACnB,cAAc,OAAO;EACvB;EACA,KAAK,aAAaC,0CAAiB,EAAE,WAAW,OAAO,UAAU,CAAC;EAElE,KAAK,UAAU,OAAO,WAAW;EAEjC,IAAI,CAAC,KAAK,OAAO,cAAc,CAAC,KAAK,OAAO,SAAS;GACnD,QAAQ,MACN,8EACF;GACA,KAAK,UAAU;EACjB;CACF;CAEA,MAAgB,kBAAkB,SAAsC;EACtE,IAAI,CAAC,QAAQ,KAAK,WAAW,KAAK,OAAO,WAAW,GAClD;EAGF,MAAM,8DAAiC,QAAQ,YAAY,QAAQ;EACnE,IACEC,+CAAuB,YACvBA,+CAAuB,KAAK,OAAO,cAEnC;EAGF,MAAM,WAAW,KAAK,OAAO;EAC7B,IAAI,YAAY,CAAC,SAAS,OAAO,GAC/B;EAGF,MAAM,QAAQ,KAAK,QAAQ,SAAS,QAAQ;EAE5C,IAAI,KAAK,OAAO,SAAS;GACvB,MAAM,KAAK,OAAO,QAAQ,KAAK;GAC/B;EACF;EAEA,MAAMC,2CACJ,KAAK,YACL,KAAK,OAAO,YACZ,OACA;GACE,SAAS,KAAK,OAAO;GACrB,YAAY,KAAK,OAAO;GACxB,cAAc,KAAK,OAAO;GAC1B,OAAO;EACT,CACF;CACF;CAEA,AAAQ,QACN,SACA,UACe;EACf,MAAM,EACJ,UAAU,WACV,UACA,SACA,QACA,GAAG,SACD,QAAQ,cAAc,CAAC;EAE3B,OAAO;GACL,OAAO,QAAQ;GACf;GACA,GAAI,OAAO,aAAa,YAAY,EAAE,SAAS;GAC/C,GAAI,OAAO,YAAY,YAAY,EAAE,QAAQ;GAC7C,GAAI,OAAO,WAAW,YAAY,EAAE,OAAO;GAC3C,GAAI,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,KAAK,EAAE,YAAY,KAAK;GACvD,WAAW,QAAQ;GACnB,GAAI,QAAQ,WAAW,EAAE,OAAO,QAAQ,QAAQ;EAClD;CACF;AACF"}