"use strict";
/**
 * SentinelDestination -- Microsoft Sentinel audit output adapters.
 *
 * Delivers governance events to Microsoft Sentinel via two API variants,
 * selectable at construction time via `apiVariant`:
 *
 *   'log-analytics-collector'
 *     Legacy Log Analytics Data Collector API (2016-04-01).
 *     Auth: HMAC-SHA256 signed with workspace shared key.
 *     Endpoint: https://<workspaceId>.ods.opinsights.azure.com/api/logs?api-version=2016-04-01
 *     Max body: 30 MB (hard Azure limit). Default: gzip compressed.
 *     Returns 200 on accept.
 *
 *   'logs-ingestion'
 *     Logs Ingestion API (2023-01-01). Azure AD / managed-identity auth.
 *     Auth: Bearer token from caller-supplied tokenProvider callback.
 *     This adapter does NOT implement the Azure AD OAuth flow -- the caller
 *     supplies a token via tokenProvider(() => Promise<string>).
 *     Endpoint: https://<dceEndpoint>/dataCollectionRules/<dcrImmutableId>/streams/<streamName>?api-version=2023-01-01
 *     Max body: 1 MB (hard Azure limit). Default: gzip compressed.
 *     Returns 204 No Content on success.
 *
 * Design decisions:
 *   - No Azure SDK. Plain HTTPS POST via Node built-ins (no supply-chain risk).
 *   - Workspace shared key is NEVER stored in plaintext. Only a KMS reference
 *     (sharedKeyRef) is held; resolved at send-time via the injected SecretResolver.
 *   - Retry semantics mirror datadog-logs.ts and splunk-hec.ts:
 *     429 (rate-limited) + 5xx = retryable. Retry-After honored on 429.
 *     4xx other than 429 = non-retryable; DLQ immediately.
 *   - Gzip compression applied by default. Configurable.
 *   - packBinding is mandatory. Empty binding throws UnboundDataError before any
 *     HTTP request (pack-binding primacy, Thomas Directive 2026-04-22).
 *   - Workspace shared key and bearer token are NEVER emitted to any log stream,
 *     error message, or DLQ entry (PT-SENTINEL-1, PT-SENTINEL-2).
 *   - Body chunking by byte size:
 *     Log Analytics: 30 MB max per request.
 *     Logs Ingestion: 1 MB max per request.
 *     Events larger than a single-request max are individually DLQ'd with
 *     an explicit error; no silent truncation.
 *
 * HMAC-SHA256 signing (Log Analytics variant):
 *   String-to-sign: METHOD + "\n" + CONTENT-LENGTH + "\n" + "application/json" + "\n" +
 *                   "x-ms-date:" + RFC1123-date + "\n" + RESOURCE
 *   RESOURCE: "/api/logs"
 *   Header: Authorization: SharedKey <workspaceId>:<base64(hmac)>
 *
 * @connexum/ai-governance
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.SentinelLogsIngestionDestination = exports.SentinelLogAnalyticsDestination = void 0;
exports.validateLogType = validateLogType;
exports.validateEndpointUrl = validateEndpointUrl;
exports.toRFC1123 = toRFC1123;
exports.buildStringToSign = buildStringToSign;
exports.buildLogAnalyticsAuthHeader = buildLogAnalyticsAuthHeader;
exports.chunkEventsByBytes = chunkEventsByBytes;
exports.exponentialDelay = exponentialDelay;
exports.parseRetryAfter = parseRetryAfter;
const https = __importStar(require("https"));
const http = __importStar(require("http"));
const zlib = __importStar(require("zlib"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const log_1 = require("../../util/log");
const index_1 = require("../../errors/index");
const log = (0, log_1.createLogger)('sentinel');
// ---------------------------------------------------------------------------
// Azure-specific limits
// ---------------------------------------------------------------------------
/** Azure Log Analytics Data Collector API hard max (30 MB). */
const LOG_ANALYTICS_MAX_BYTES = 30_000_000;
/** Conservative default for Log Analytics (28 MB, leaves safety margin). */
const LOG_ANALYTICS_DEFAULT_MAX_BYTES = 28_000_000;
/** Azure Logs Ingestion API hard max (1 MB). */
const LOGS_INGESTION_MAX_BYTES = 1_000_000;
/** Conservative default for Logs Ingestion (900 KB, leaves safety margin). */
const LOGS_INGESTION_DEFAULT_MAX_BYTES = 900_000;
/** Log Analytics API version. */
const LOG_ANALYTICS_API_VERSION = '2016-04-01';
/** Logs Ingestion API version. */
const LOGS_INGESTION_API_VERSION = '2023-01-01';
/** Log Analytics resource path used in HMAC string-to-sign. */
const LOG_ANALYTICS_RESOURCE = '/api/logs';
/** HTTP status codes that are retryable. */
const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
// ---------------------------------------------------------------------------
// Validation helpers (pure, exported for tests)
// ---------------------------------------------------------------------------
/**
 * Validate a Log-Type header value per Azure requirements:
 *   - 1 to 100 characters
 *   - Alphanumeric characters and underscores only
 *   - Must start with a letter
 */
function validateLogType(logType) {
    if (!logType || logType.length === 0) {
        throw new Error('SentinelLogAnalyticsDestination: logType is required. ' +
            'Provide a 1-100 char alphanumeric+underscore string starting with a letter.');
    }
    if (logType.length > 100) {
        throw new Error(`SentinelLogAnalyticsDestination: logType "${logType}" exceeds 100 characters (got ${logType.length}).`);
    }
    if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(logType)) {
        throw new Error(`SentinelLogAnalyticsDestination: logType "${logType}" is invalid. ` +
            'Must start with a letter and contain only alphanumeric characters and underscores.');
    }
}
/**
 * Validate that a URL is HTTPS unless allowInsecure is explicitly set.
 * Throws a descriptive error on failure.
 */
function validateEndpointUrl(url, allowInsecure, label) {
    let parsed;
    try {
        parsed = new URL(url);
    }
    catch {
        throw new Error(`${label}: malformed endpoint URL "${url}". ` +
            'Provide a valid HTTPS URL.');
    }
    if (!allowInsecure && parsed.protocol !== 'https:') {
        throw new Error(`${label}: endpoint URL must use HTTPS. Received protocol: "${parsed.protocol}". ` +
            'Set allowInsecure:true only in test environments.');
    }
}
/**
 * Compute the RFC1123 date string required by Azure Log Analytics.
 * Format: "Thu, 30 Apr 2026 12:00:00 GMT"
 */
function toRFC1123(date) {
    return date.toUTCString();
}
/**
 * Build the canonical string-to-sign for Log Analytics HMAC-SHA256.
 *
 * Per Azure documentation:
 *   METHOD + "\n" + CONTENT-LENGTH + "\n" + "application/json" + "\n" +
 *   "x-ms-date:" + rfc1123date + "\n" + RESOURCE
 *
 * Where RESOURCE is "/api/logs".
 */
function buildStringToSign(method, contentLength, rfc1123date) {
    return [
        method,
        contentLength.toString(),
        'application/json',
        `x-ms-date:${rfc1123date}`,
        LOG_ANALYTICS_RESOURCE,
    ].join('\n');
}
/**
 * Compute the Authorization header value for the Log Analytics Data Collector API.
 *
 * Authorization: SharedKey <workspaceId>:<base64(HMAC-SHA256(stringToSign, sharedKey))>
 *
 * The sharedKey parameter is the decoded raw bytes (Buffer from base64).
 * This function is exported for test fixture verification only.
 * It must never be called with a key sourced from config state directly.
 */
function buildLogAnalyticsAuthHeader(workspaceId, stringToSign, sharedKeyBytes) {
    const signature = crypto
        .createHmac('sha256', sharedKeyBytes)
        .update(stringToSign, 'utf8')
        .digest('base64');
    return `SharedKey ${workspaceId}:${signature}`;
}
/**
 * Chunk an array of EventPayload objects into sub-arrays that respect
 * the per-request byte size limit.
 *
 * Events larger than maxBytesPerRequest are separated into a 'rejects' array
 * (they will be DLQ'd individually with an explicit oversized error).
 * No silent truncation occurs.
 */
function chunkEventsByBytes(events, maxBytesPerRequest) {
    const chunks = [];
    const oversized = [];
    let current = [];
    // Start with 2 for the outer array brackets [].
    let currentBytes = 2;
    for (const evt of events) {
        const evtJson = JSON.stringify(evt);
        const evtBytes = evtJson.length + (current.length > 0 ? 1 : 0); // +1 for comma separator
        // A single event that exceeds the max on its own is oversized.
        if (evtBytes + 2 > maxBytesPerRequest) {
            oversized.push(evt);
            continue;
        }
        if (current.length > 0 && currentBytes + evtBytes > maxBytesPerRequest) {
            chunks.push(current);
            current = [];
            currentBytes = 2;
        }
        current.push(evt);
        currentBytes += evtBytes;
    }
    if (current.length > 0) {
        chunks.push(current);
    }
    return { chunks, oversized };
}
// ---------------------------------------------------------------------------
// Shared pure helpers (exported for test use)
// ---------------------------------------------------------------------------
/**
 * Compute exponential backoff with 25% jitter.
 *   delay = base * 2^(attempt-1) + random(0, base * 0.25)
 */
function exponentialDelay(attempt, baseMs) {
    const base = baseMs * Math.pow(2, attempt - 1);
    const jitter = Math.floor(Math.random() * base * 0.25);
    return base + jitter;
}
/**
 * Parse the Retry-After header value.
 * Accepts: integer seconds ("30") or HTTP-date ("Wed, 21 Oct 2015 07:28:00 GMT").
 * Returns milliseconds to wait, or undefined if unparseable.
 */
function parseRetryAfter(headerValue) {
    if (!headerValue)
        return undefined;
    const seconds = parseInt(headerValue, 10);
    if (!Number.isNaN(seconds) && seconds > 0) {
        return seconds * 1_000;
    }
    const date = new Date(headerValue);
    if (!Number.isNaN(date.getTime())) {
        const diff = date.getTime() - Date.now();
        return diff > 0 ? diff : undefined;
    }
    return undefined;
}
// ---------------------------------------------------------------------------
// Log Analytics Destination
// ---------------------------------------------------------------------------
class SentinelLogAnalyticsDestination {
    cfg;
    resolver;
    dlqFile;
    endpointUrl;
    constructor(config, resolver) {
        // Pack-binding primacy: empty binding throws UnboundDataError before anything else.
        if (!config.packBinding || config.packBinding.length === 0) {
            throw new index_1.UnboundDataError('SentinelLogAnalyticsDestination.config', 'unknown');
        }
        if (!config.workspaceId || config.workspaceId.trim() === '') {
            throw new Error('SentinelLogAnalyticsDestination: workspaceId is required.');
        }
        if (!config.sharedKeyRef || config.sharedKeyRef.trim() === '') {
            throw new Error('SentinelLogAnalyticsDestination: sharedKeyRef is required.');
        }
        // Validate logType at construction (PT-SENTINEL-3).
        validateLogType(config.logType);
        const allowInsecure = config.allowInsecure ?? false;
        // Derive endpoint URL.
        let endpointUrl;
        if (config.baseUrl) {
            validateEndpointUrl(config.baseUrl, allowInsecure, 'SentinelLogAnalyticsDestination');
            endpointUrl = config.baseUrl;
        }
        else {
            endpointUrl = `https://${config.workspaceId}.ods.opinsights.azure.com${LOG_ANALYTICS_RESOURCE}?api-version=${LOG_ANALYTICS_API_VERSION}`;
        }
        this.endpointUrl = endpointUrl;
        this.resolver = resolver;
        this.cfg = {
            baseUrl: config.baseUrl ?? '',
            timeGeneratedField: '',
            maxBytesPerRequest: LOG_ANALYTICS_DEFAULT_MAX_BYTES,
            compress: true,
            allowInsecure: false,
            timeoutMs: 10_000,
            maxRetries: 3,
            retryDelayMs: 1_000,
            ...config,
        };
        if (!fs.existsSync(this.cfg.dlqDir)) {
            fs.mkdirSync(this.cfg.dlqDir, { recursive: true });
        }
        this.dlqFile = path.join(this.cfg.dlqDir, 'sentinel-log-analytics-dlq.jsonl');
    }
    // ---------------------------------------------------------------------------
    // Public API
    // ---------------------------------------------------------------------------
    /**
     * Deliver one or more governance events to Sentinel via Log Analytics API.
     *
     * Chunks events by byte size (max 30 MB per request by default).
     * Events that individually exceed the max are DLQ'd with an explicit error.
     * The workspace shared key is resolved per-call and never cached on the instance.
     */
    async deliver(events) {
        if (events.length === 0) {
            return { success: true, eventCount: 0, attempts: 0 };
        }
        // Resolve shared key at deliver-time; never cached on the instance (PT-SENTINEL-1).
        let sharedKeyBase64;
        try {
            sharedKeyBase64 = await this.resolver(this.cfg.sharedKeyRef);
        }
        catch (err) {
            const error = `Secret resolver failed for sharedKeyRef: ${err.message}`;
            this.writeDLQ(events, error, 0, false);
            return { success: false, eventCount: events.length, attempts: 0, error, retryable: false };
        }
        const effectiveMaxBytes = Math.min(this.cfg.maxBytesPerRequest, LOG_ANALYTICS_MAX_BYTES);
        const { chunks, oversized } = chunkEventsByBytes(events, effectiveMaxBytes);
        // DLQ oversized events immediately with explicit error.
        for (const evt of oversized) {
            const oversizeError = `Oversized event: exceeds Log Analytics max request size (${effectiveMaxBytes} bytes). No silent truncation.`;
            this.writeDLQ([evt], oversizeError, 0, false);
            log.error('Sentinel Log Analytics: oversized event sent to DLQ', {
                eventType: evt.type,
                maxBytes: effectiveMaxBytes,
            });
        }
        if (chunks.length === 0) {
            if (oversized.length > 0) {
                return {
                    success: false,
                    eventCount: events.length,
                    attempts: 0,
                    error: `All ${oversized.length} oversized events sent to DLQ. No silent truncation.`,
                    retryable: false,
                };
            }
            return { success: true, eventCount: 0, attempts: 0 };
        }
        let totalAttempts = 0;
        let lastStatus;
        for (const chunk of chunks) {
            const result = await this.postChunk(chunk, sharedKeyBase64);
            totalAttempts += result.attempts;
            if (result.statusCode !== undefined) {
                lastStatus = result.statusCode;
            }
            if (!result.success) {
                return {
                    success: false,
                    eventCount: events.length,
                    statusCode: lastStatus,
                    attempts: totalAttempts,
                    error: result.error,
                    retryable: result.retryable,
                };
            }
        }
        return {
            success: true,
            eventCount: events.length,
            statusCode: lastStatus,
            attempts: totalAttempts,
            deliveredAt: new Date().toISOString(),
            retryable: false,
        };
    }
    /**
     * Flush alias for API symmetry. Log Analytics destination does not internally
     * buffer; deliver is always immediate. flush() is a no-op that returns success.
     */
    async flush() {
        return { success: true, eventCount: 0, attempts: 0 };
    }
    /** Retrieve dead-letter queue entries (most recent `limit` entries). */
    getDLQ(limit = 100) {
        if (!fs.existsSync(this.dlqFile))
            return [];
        try {
            const lines = fs.readFileSync(this.dlqFile, 'utf-8')
                .trim()
                .split('\n')
                .filter(Boolean);
            return lines
                .slice(-limit)
                .map(line => {
                try {
                    return JSON.parse(line);
                }
                catch {
                    return null;
                }
            })
                .filter((e) => e !== null);
        }
        catch {
            return [];
        }
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    /**
     * POST one chunk of events to the Log Analytics Data Collector API.
     *
     * The shared key is accepted as a parameter and only used for HMAC signing.
     * It is never stored in instance state or written to any log (PT-SENTINEL-1).
     */
    async postChunk(chunk, sharedKeyBase64) {
        const bodyStr = JSON.stringify(chunk);
        const sharedKeyBytes = Buffer.from(sharedKeyBase64, 'base64');
        let lastError = '';
        let lastStatus;
        let retryable = true;
        for (let attempt = 1; attempt <= this.cfg.maxRetries; attempt++) {
            try {
                const rfc1123date = toRFC1123(new Date());
                const contentLength = Buffer.byteLength(bodyStr, 'utf-8');
                const stringToSign = buildStringToSign('POST', contentLength, rfc1123date);
                const authHeader = buildLogAnalyticsAuthHeader(this.cfg.workspaceId, stringToSign, sharedKeyBytes);
                const result = await this.httpPost(bodyStr, authHeader, rfc1123date, this.cfg.compress);
                lastStatus = result.statusCode;
                if (result.statusCode >= 200 && result.statusCode < 300) {
                    return {
                        success: true,
                        eventCount: chunk.length,
                        statusCode: result.statusCode,
                        attempts: attempt,
                        deliveredAt: new Date().toISOString(),
                        retryable: false,
                    };
                }
                if (result.statusCode === 429) {
                    lastError = 'HTTP 429 (rate limited)';
                    retryable = true;
                    if (attempt < this.cfg.maxRetries) {
                        const retryAfterMs = parseRetryAfter(result.retryAfterHeader);
                        const backoff = retryAfterMs ?? exponentialDelay(attempt, this.cfg.retryDelayMs);
                        log.warn('Sentinel Log Analytics rate limited, waiting before retry', {
                            attempt,
                            waitMs: backoff,
                        });
                        await new Promise(r => setTimeout(r, backoff));
                    }
                    continue;
                }
                if (RETRYABLE_STATUS_CODES.has(result.statusCode)) {
                    lastError = `HTTP ${result.statusCode}`;
                    retryable = true;
                }
                else {
                    // 4xx other than 429: terminal, DLQ immediately.
                    lastError = `HTTP ${result.statusCode} (non-retryable)`;
                    retryable = false;
                    log.error('Sentinel Log Analytics non-retryable error', {
                        status: result.statusCode,
                    });
                    break;
                }
            }
            catch (err) {
                lastError = err.message ?? 'Connection error';
                retryable = true;
                log.warn('Sentinel Log Analytics connection error', {
                    attempt,
                    error: lastError,
                });
            }
            if (attempt < this.cfg.maxRetries && retryable) {
                const backoff = exponentialDelay(attempt, this.cfg.retryDelayMs);
                await new Promise(r => setTimeout(r, backoff));
            }
        }
        const error = lastError || `HTTP ${lastStatus}`;
        this.writeDLQ(chunk, error, this.cfg.maxRetries, retryable);
        return {
            success: false,
            eventCount: chunk.length,
            statusCode: lastStatus,
            attempts: this.cfg.maxRetries,
            error,
            retryable,
        };
    }
    /**
     * Execute the HTTPS POST to the Log Analytics Data Collector API.
     *
     * The Authorization header contains the HMAC signature derived from the
     * shared key. The raw shared key bytes are accepted as a parameter and
     * are NEVER logged or persisted in any way (PT-SENTINEL-1).
     */
    httpPost(body, authHeader, rfc1123date, compress) {
        return new Promise((resolve, reject) => {
            const url = new URL(this.endpointUrl);
            const isHttps = url.protocol === 'https:';
            const transport = isHttps ? https : http;
            const dispatch = (finalBody, extraHeaders) => {
                const headers = {
                    'Content-Type': 'application/json',
                    'Content-Length': finalBody.length.toString(),
                    'Authorization': authHeader,
                    'Log-Type': this.cfg.logType,
                    'x-ms-date': rfc1123date,
                    'User-Agent': '@connexum/ai-governance sentinel/1.0',
                    ...extraHeaders,
                };
                if (this.cfg.timeGeneratedField) {
                    headers['time-generated-field'] = this.cfg.timeGeneratedField;
                }
                const req = transport.request({
                    hostname: url.hostname,
                    port: url.port || (isHttps ? 443 : 80),
                    path: url.pathname + url.search,
                    method: 'POST',
                    headers,
                    timeout: this.cfg.timeoutMs,
                }, (res) => {
                    let rb = '';
                    res.on('data', (chunk) => { rb += chunk.toString(); });
                    res.on('end', () => {
                        resolve({
                            statusCode: res.statusCode ?? 0,
                            body: rb,
                            retryAfterHeader: res.headers['retry-after'],
                        });
                    });
                });
                req.on('error', (err) => {
                    const raw = err.message ?? '';
                    // Ensure shared key reference never surfaces in error propagation.
                    if (raw.includes(this.cfg.sharedKeyRef)) {
                        reject(new Error('Connection error (details redacted for security)'));
                    }
                    else {
                        reject(err);
                    }
                });
                req.on('timeout', () => {
                    req.destroy(new Error('Sentinel Log Analytics request timeout'));
                });
                req.write(finalBody);
                req.end();
            };
            const bodyBuf = Buffer.from(body, 'utf-8');
            if (compress) {
                zlib.gzip(bodyBuf, (err, compressed) => {
                    if (err) {
                        reject(err);
                        return;
                    }
                    dispatch(compressed, {
                        'Content-Encoding': 'gzip',
                        'Content-Length': compressed.length.toString(),
                    });
                });
            }
            else {
                dispatch(bodyBuf, {});
            }
        });
    }
    writeDLQ(events, error, attempts, retryable) {
        const entry = {
            events,
            error,
            attempts,
            failedAt: new Date().toISOString(),
            retryable,
        };
        try {
            fs.appendFileSync(this.dlqFile, JSON.stringify(entry) + '\n');
        }
        catch {
            log.error('Failed to write Sentinel Log Analytics DLQ entry', { dlqFile: this.dlqFile });
        }
    }
}
exports.SentinelLogAnalyticsDestination = SentinelLogAnalyticsDestination;
// ---------------------------------------------------------------------------
// Logs Ingestion Destination
// ---------------------------------------------------------------------------
class SentinelLogsIngestionDestination {
    cfg;
    dlqFile;
    endpointUrl;
    constructor(config) {
        // Pack-binding primacy: empty binding throws UnboundDataError before anything else.
        if (!config.packBinding || config.packBinding.length === 0) {
            throw new index_1.UnboundDataError('SentinelLogsIngestionDestination.config', 'unknown');
        }
        if (!config.dceEndpoint || config.dceEndpoint.trim() === '') {
            throw new Error('SentinelLogsIngestionDestination: dceEndpoint is required.');
        }
        if (!config.dcrImmutableId || config.dcrImmutableId.trim() === '') {
            throw new Error('SentinelLogsIngestionDestination: dcrImmutableId is required.');
        }
        if (!config.streamName || config.streamName.trim() === '') {
            throw new Error('SentinelLogsIngestionDestination: streamName is required.');
        }
        if (typeof config.tokenProvider !== 'function') {
            throw new Error('SentinelLogsIngestionDestination: tokenProvider must be a function returning Promise<string>. ' +
                'This adapter does not implement Azure AD OAuth flow; the caller supplies the token.');
        }
        const allowInsecure = config.allowInsecure ?? false;
        // Normalize DCE endpoint: strip trailing slash.
        const dceBase = config.dceEndpoint.replace(/\/$/, '');
        validateEndpointUrl(dceBase, allowInsecure, 'SentinelLogsIngestionDestination');
        this.endpointUrl = (`${dceBase}/dataCollectionRules/${config.dcrImmutableId}` +
            `/streams/${config.streamName}?api-version=${LOGS_INGESTION_API_VERSION}`);
        this.cfg = {
            maxBytesPerRequest: LOGS_INGESTION_DEFAULT_MAX_BYTES,
            compress: true,
            allowInsecure: false,
            timeoutMs: 10_000,
            maxRetries: 3,
            retryDelayMs: 1_000,
            ...config,
        };
        if (!fs.existsSync(this.cfg.dlqDir)) {
            fs.mkdirSync(this.cfg.dlqDir, { recursive: true });
        }
        this.dlqFile = path.join(this.cfg.dlqDir, 'sentinel-logs-ingestion-dlq.jsonl');
    }
    // ---------------------------------------------------------------------------
    // Public API
    // ---------------------------------------------------------------------------
    /**
     * Deliver one or more governance events to Sentinel via Logs Ingestion API.
     *
     * Chunks events by byte size (max 1 MB per request by default).
     * Events that individually exceed the max are DLQ'd with an explicit error.
     * The Bearer token is obtained per-call via tokenProvider and never cached
     * on the instance (PT-SENTINEL-2).
     */
    async deliver(events) {
        if (events.length === 0) {
            return { success: true, eventCount: 0, attempts: 0 };
        }
        // Obtain Bearer token at deliver-time; never cached on the instance (PT-SENTINEL-2).
        let token;
        try {
            token = await this.cfg.tokenProvider();
        }
        catch (err) {
            const error = `tokenProvider failed: ${err.message}`;
            this.writeDLQ(events, error, 0, false);
            return { success: false, eventCount: events.length, attempts: 0, error, retryable: false };
        }
        const effectiveMaxBytes = Math.min(this.cfg.maxBytesPerRequest, LOGS_INGESTION_MAX_BYTES);
        const { chunks, oversized } = chunkEventsByBytes(events, effectiveMaxBytes);
        // DLQ oversized events immediately with explicit error.
        for (const evt of oversized) {
            const oversizeError = `Oversized event: exceeds Logs Ingestion max request size (${effectiveMaxBytes} bytes). No silent truncation.`;
            this.writeDLQ([evt], oversizeError, 0, false);
            log.error('Sentinel Logs Ingestion: oversized event sent to DLQ', {
                eventType: evt.type,
                maxBytes: effectiveMaxBytes,
            });
        }
        if (chunks.length === 0) {
            if (oversized.length > 0) {
                return {
                    success: false,
                    eventCount: events.length,
                    attempts: 0,
                    error: `All ${oversized.length} oversized events sent to DLQ. No silent truncation.`,
                    retryable: false,
                };
            }
            return { success: true, eventCount: 0, attempts: 0 };
        }
        let totalAttempts = 0;
        let lastStatus;
        for (const chunk of chunks) {
            const result = await this.postChunk(chunk, token);
            totalAttempts += result.attempts;
            if (result.statusCode !== undefined) {
                lastStatus = result.statusCode;
            }
            if (!result.success) {
                return {
                    success: false,
                    eventCount: events.length,
                    statusCode: lastStatus,
                    attempts: totalAttempts,
                    error: result.error,
                    retryable: result.retryable,
                };
            }
        }
        return {
            success: true,
            eventCount: events.length,
            statusCode: lastStatus,
            attempts: totalAttempts,
            deliveredAt: new Date().toISOString(),
            retryable: false,
        };
    }
    /**
     * Flush alias for API symmetry. Logs Ingestion destination does not internally
     * buffer; deliver is always immediate. flush() is a no-op that returns success.
     */
    async flush() {
        return { success: true, eventCount: 0, attempts: 0 };
    }
    /** Retrieve dead-letter queue entries (most recent `limit` entries). */
    getDLQ(limit = 100) {
        if (!fs.existsSync(this.dlqFile))
            return [];
        try {
            const lines = fs.readFileSync(this.dlqFile, 'utf-8')
                .trim()
                .split('\n')
                .filter(Boolean);
            return lines
                .slice(-limit)
                .map(line => {
                try {
                    return JSON.parse(line);
                }
                catch {
                    return null;
                }
            })
                .filter((e) => e !== null);
        }
        catch {
            return [];
        }
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    /**
     * POST one chunk of events to the Logs Ingestion API.
     *
     * The Bearer token is accepted as a parameter and used only in the
     * Authorization header. It is NEVER stored in instance state or logged
     * (PT-SENTINEL-2).
     */
    async postChunk(chunk, token) {
        const bodyStr = JSON.stringify(chunk);
        let lastError = '';
        let lastStatus;
        let retryable = true;
        for (let attempt = 1; attempt <= this.cfg.maxRetries; attempt++) {
            try {
                const result = await this.httpPost(bodyStr, token, this.cfg.compress);
                lastStatus = result.statusCode;
                // Logs Ingestion API returns 204 No Content on success.
                if (result.statusCode === 204 || (result.statusCode >= 200 && result.statusCode < 300)) {
                    return {
                        success: true,
                        eventCount: chunk.length,
                        statusCode: result.statusCode,
                        attempts: attempt,
                        deliveredAt: new Date().toISOString(),
                        retryable: false,
                    };
                }
                if (result.statusCode === 429) {
                    lastError = 'HTTP 429 (rate limited)';
                    retryable = true;
                    if (attempt < this.cfg.maxRetries) {
                        const retryAfterMs = parseRetryAfter(result.retryAfterHeader);
                        const backoff = retryAfterMs ?? exponentialDelay(attempt, this.cfg.retryDelayMs);
                        log.warn('Sentinel Logs Ingestion rate limited, waiting before retry', {
                            attempt,
                            waitMs: backoff,
                        });
                        await new Promise(r => setTimeout(r, backoff));
                    }
                    continue;
                }
                if (RETRYABLE_STATUS_CODES.has(result.statusCode)) {
                    lastError = `HTTP ${result.statusCode}`;
                    retryable = true;
                }
                else {
                    // 4xx other than 429: terminal, DLQ immediately.
                    lastError = `HTTP ${result.statusCode} (non-retryable)`;
                    retryable = false;
                    log.error('Sentinel Logs Ingestion non-retryable error', {
                        status: result.statusCode,
                    });
                    break;
                }
            }
            catch (err) {
                lastError = err.message ?? 'Connection error';
                retryable = true;
                log.warn('Sentinel Logs Ingestion connection error', {
                    attempt,
                    error: lastError,
                });
            }
            if (attempt < this.cfg.maxRetries && retryable) {
                const backoff = exponentialDelay(attempt, this.cfg.retryDelayMs);
                await new Promise(r => setTimeout(r, backoff));
            }
        }
        const error = lastError || `HTTP ${lastStatus}`;
        this.writeDLQ(chunk, error, this.cfg.maxRetries, retryable);
        return {
            success: false,
            eventCount: chunk.length,
            statusCode: lastStatus,
            attempts: this.cfg.maxRetries,
            error,
            retryable,
        };
    }
    /**
     * Execute the HTTPS POST to the Logs Ingestion API.
     *
     * The Bearer token is accepted as a parameter and used only in the
     * Authorization header. It is NEVER logged or persisted (PT-SENTINEL-2).
     */
    httpPost(body, token, compress) {
        return new Promise((resolve, reject) => {
            const url = new URL(this.endpointUrl);
            const isHttps = url.protocol === 'https:';
            const transport = isHttps ? https : http;
            const dispatch = (finalBody, extraHeaders) => {
                const headers = {
                    'Content-Type': 'application/json',
                    'Content-Length': finalBody.length.toString(),
                    // Bearer token used here only; never logged.
                    'Authorization': `Bearer ${token}`,
                    'User-Agent': '@connexum/ai-governance sentinel/1.0',
                    ...extraHeaders,
                };
                const req = transport.request({
                    hostname: url.hostname,
                    port: url.port || (isHttps ? 443 : 80),
                    path: url.pathname + url.search,
                    method: 'POST',
                    headers,
                    timeout: this.cfg.timeoutMs,
                }, (res) => {
                    let rb = '';
                    res.on('data', (chunk) => { rb += chunk.toString(); });
                    res.on('end', () => {
                        resolve({
                            statusCode: res.statusCode ?? 0,
                            body: rb,
                            retryAfterHeader: res.headers['retry-after'],
                        });
                    });
                });
                req.on('error', (err) => {
                    const raw = err.message ?? '';
                    // Ensure token never surfaces in error propagation.
                    if (raw.includes(token)) {
                        reject(new Error('Connection error (details redacted for security)'));
                    }
                    else {
                        reject(err);
                    }
                });
                req.on('timeout', () => {
                    req.destroy(new Error('Sentinel Logs Ingestion request timeout'));
                });
                req.write(finalBody);
                req.end();
            };
            const bodyBuf = Buffer.from(body, 'utf-8');
            if (compress) {
                zlib.gzip(bodyBuf, (err, compressed) => {
                    if (err) {
                        reject(err);
                        return;
                    }
                    dispatch(compressed, {
                        'Content-Encoding': 'gzip',
                        'Content-Length': compressed.length.toString(),
                    });
                });
            }
            else {
                dispatch(bodyBuf, {});
            }
        });
    }
    writeDLQ(events, error, attempts, retryable) {
        const entry = {
            events,
            error,
            attempts,
            failedAt: new Date().toISOString(),
            retryable,
        };
        try {
            fs.appendFileSync(this.dlqFile, JSON.stringify(entry) + '\n');
        }
        catch {
            log.error('Failed to write Sentinel Logs Ingestion DLQ entry', { dlqFile: this.dlqFile });
        }
    }
}
exports.SentinelLogsIngestionDestination = SentinelLogsIngestionDestination;
//# sourceMappingURL=sentinel.js.map