"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnterpriseApiKeyService = exports.ENTERPRISE_API_KEY_PREFIX = void 0;
exports.extractEnterpriseApiKeyToken = extractEnterpriseApiKeyToken;
exports.requestHasEnterpriseApiKeyCredential = requestHasEnterpriseApiKeyCredential;
const crypto_1 = __importDefault(require("crypto"));
const enterpriseDb_1 = require("./enterpriseDb");
const enterpriseAuditService_1 = require("./enterpriseAuditService");
exports.ENTERPRISE_API_KEY_PREFIX = 'spak_';
const DEFAULT_API_KEY_SCOPES = ['trace:read', 'trace:write', 'agent:run', 'report:read'];
function nowMs() {
    return Date.now();
}
function sanitizeId(value) {
    if (typeof value !== 'string')
        return '';
    return value.trim().replace(/[^a-zA-Z0-9._:-]/g, '').slice(0, 128);
}
function sanitizeScope(value) {
    if (typeof value !== 'string')
        return '';
    const trimmed = value.trim();
    if (trimmed === '*')
        return '*';
    return trimmed.replace(/[^a-zA-Z0-9._:-]/g, '').slice(0, 128);
}
function safeText(value, fallback) {
    if (typeof value !== 'string')
        return fallback;
    const trimmed = value.trim().replace(/[\r\n]/g, '').slice(0, 120);
    return trimmed || fallback;
}
function normalizeScopes(input, fallback = DEFAULT_API_KEY_SCOPES) {
    const raw = Array.isArray(input)
        ? input
        : typeof input === 'string'
            ? input.split(/[,\s]+/)
            : [];
    const scopes = raw
        .map(scope => sanitizeScope(scope))
        .filter(Boolean);
    return scopes.length > 0 ? [...new Set(scopes)] : [...fallback];
}
function parseScopes(value) {
    try {
        return normalizeScopes(JSON.parse(value), []);
    }
    catch {
        return normalizeScopes(value, []);
    }
}
function normalizeExpiresAt(value) {
    if (value === undefined || value === null || value === '')
        return undefined;
    const parsed = typeof value === 'number'
        ? value
        : typeof value === 'string' && /^\d+$/.test(value.trim())
            ? Number.parseInt(value, 10)
            : Date.parse(String(value));
    if (!Number.isFinite(parsed)) {
        throw new Error('expiresAt must be a Unix millisecond timestamp or ISO date string');
    }
    if (parsed <= nowMs()) {
        throw new Error('expiresAt must be in the future');
    }
    return parsed;
}
function hashTokenSecret(id, secret) {
    return crypto_1.default.createHash('sha256').update(`${id}.${secret}`).digest('hex');
}
function safeEquals(a, b) {
    const aBuf = Buffer.from(a);
    const bBuf = Buffer.from(b);
    return aBuf.length === bBuf.length && crypto_1.default.timingSafeEqual(aBuf, bBuf);
}
function parseApiKeyToken(token) {
    if (!token?.startsWith(exports.ENTERPRISE_API_KEY_PREFIX))
        return null;
    const payload = token.slice(exports.ENTERPRISE_API_KEY_PREFIX.length);
    const separator = payload.indexOf('.');
    if (separator <= 0)
        return null;
    const id = payload.slice(0, separator);
    const secret = payload.slice(separator + 1);
    return sanitizeId(id) === id && secret.length >= 32 ? { id, secret } : null;
}
function extractEnterpriseApiKeyToken(req) {
    const authHeader = req.headers.authorization;
    if (typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
        const token = authHeader.slice('Bearer '.length).trim();
        if (token.startsWith(exports.ENTERPRISE_API_KEY_PREFIX))
            return token;
    }
    const headerKey = req.headers['x-api-key'];
    if (typeof headerKey === 'string' && headerKey.trim().startsWith(exports.ENTERPRISE_API_KEY_PREFIX)) {
        return headerKey.trim();
    }
    return undefined;
}
function requestHasEnterpriseApiKeyCredential(req) {
    return Boolean(extractEnterpriseApiKeyToken(req));
}
function rowToRecord(row) {
    return {
        id: row.id,
        tenantId: row.tenant_id,
        ...(row.workspace_id ? { workspaceId: row.workspace_id } : {}),
        ...(row.owner_user_id ? { ownerUserId: row.owner_user_id } : {}),
        name: row.name,
        scopes: parseScopes(row.scopes),
        createdAt: row.created_at,
        ...(row.expires_at ? { expiresAt: row.expires_at } : {}),
        ...(row.revoked_at ? { revokedAt: row.revoked_at } : {}),
        ...(row.last_used_at ? { lastUsedAt: row.last_used_at } : {}),
    };
}
class EnterpriseApiKeyService {
    constructor(db = (0, enterpriseDb_1.openEnterpriseDb)()) {
        this.db = db;
    }
    static getInstance() {
        if (!EnterpriseApiKeyService.instance) {
            EnterpriseApiKeyService.instance = new EnterpriseApiKeyService();
        }
        return EnterpriseApiKeyService.instance;
    }
    static resetForTests() {
        EnterpriseApiKeyService.instance = undefined;
    }
    static setInstanceForTests(service) {
        EnterpriseApiKeyService.instance = service;
    }
    createApiKey(context, input = {}) {
        const requestedTenantId = sanitizeId(input.tenantId);
        if (requestedTenantId && requestedTenantId !== context.tenantId) {
            throw new Error('tenantId must match the authenticated RequestContext');
        }
        const requestedWorkspaceId = sanitizeId(input.workspaceId);
        if (requestedWorkspaceId && requestedWorkspaceId !== context.workspaceId) {
            throw new Error('workspaceId must match the authenticated RequestContext');
        }
        const tenantId = context.tenantId;
        const workspaceId = input.workspaceId === null ? undefined : context.workspaceId;
        const ownerUserId = input.ownerUserId === null
            ? undefined
            : this.existingUserId(tenantId, sanitizeId(input.ownerUserId) || context.userId);
        const scopes = normalizeScopes(input.scopes);
        const expiresAt = normalizeExpiresAt(input.expiresAt);
        const id = `ak_${crypto_1.default.randomUUID()}`;
        const secret = crypto_1.default.randomBytes(32).toString('base64url');
        const createdAt = nowMs();
        this.db.prepare(`
      INSERT INTO api_keys
        (id, tenant_id, workspace_id, owner_user_id, name, key_hash, scopes, created_at, expires_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(id, tenantId, workspaceId ?? null, ownerUserId ?? null, safeText(input.name, 'API key'), hashTokenSecret(id, secret), JSON.stringify(scopes), createdAt, expiresAt ?? null);
        (0, enterpriseAuditService_1.recordEnterpriseAuditEvent)(this.db, {
            tenantId,
            workspaceId,
            actorUserId: this.existingUserId(tenantId, context.userId),
            action: 'api_key_created',
            resourceType: 'api_key',
            resourceId: id,
            metadata: {
                name: safeText(input.name, 'API key'),
                scopes,
                expiresAt,
                ownerUserId,
            },
        });
        const row = this.getRow(id);
        if (!row)
            throw new Error('API key was not persisted');
        return {
            apiKey: rowToRecord(row),
            token: `${exports.ENTERPRISE_API_KEY_PREFIX}${id}.${secret}`,
        };
    }
    listApiKeys(context) {
        return this.db.prepare(`
      SELECT *
      FROM api_keys
      WHERE tenant_id = ?
        AND (workspace_id IS NULL OR workspace_id = ?)
      ORDER BY created_at DESC
    `).all(context.tenantId, context.workspaceId).map(rowToRecord);
    }
    revokeApiKey(context, idInput) {
        const id = sanitizeId(idInput);
        const existing = id ? this.getRowForContext(context, id) : null;
        if (!existing || existing.revoked_at)
            return null;
        const revokedAt = nowMs();
        this.db.prepare(`
      UPDATE api_keys SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL
    `).run(revokedAt, existing.id);
        (0, enterpriseAuditService_1.recordEnterpriseAuditEvent)(this.db, {
            tenantId: existing.tenant_id,
            workspaceId: existing.workspace_id ?? undefined,
            actorUserId: this.existingUserId(existing.tenant_id, context.userId),
            action: 'api_key_revoked',
            resourceType: 'api_key',
            resourceId: existing.id,
            metadata: {
                ownerUserId: existing.owner_user_id,
                scopes: parseScopes(existing.scopes),
            },
        });
        const row = this.getRow(existing.id);
        return row ? rowToRecord(row) : null;
    }
    resolveRequestIdentityFromRequest(req) {
        const parts = parseApiKeyToken(extractEnterpriseApiKeyToken(req));
        if (!parts)
            return null;
        const row = this.getRow(parts.id);
        if (!row || row.revoked_at || (row.expires_at && row.expires_at <= nowMs()))
            return null;
        const expectedHash = hashTokenSecret(parts.id, parts.secret);
        if (!safeEquals(row.key_hash, expectedHash))
            return null;
        this.db.prepare('UPDATE api_keys SET last_used_at = ? WHERE id = ?').run(nowMs(), row.id);
        return {
            userId: row.owner_user_id || row.id,
            email: '',
            subscription: 'enterprise',
            authType: 'api_key',
            tenantId: row.tenant_id,
            ...(row.workspace_id ? { workspaceId: row.workspace_id } : {}),
            roles: ['api_key'],
            scopes: parseScopes(row.scopes),
        };
    }
    getRow(id) {
        return this.db.prepare('SELECT * FROM api_keys WHERE id = ?').get(id) || null;
    }
    getRowForContext(context, id) {
        return this.db.prepare(`
      SELECT *
      FROM api_keys
      WHERE id = ?
        AND tenant_id = ?
        AND (workspace_id IS NULL OR workspace_id = ?)
    `).get(id, context.tenantId, context.workspaceId) || null;
    }
    existingUserId(tenantId, userId) {
        if (!userId)
            return undefined;
        const row = this.db.prepare('SELECT id FROM users WHERE tenant_id = ? AND id = ?').get(tenantId, userId);
        return row?.id;
    }
}
exports.EnterpriseApiKeyService = EnterpriseApiKeyService;
//# sourceMappingURL=enterpriseApiKeyService.js.map