"use strict";
// backend/src/services/providerManager/providerStore.ts
// SPDX-License-Identifier: AGPL-3.0-or-later
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;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProviderStore = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto_1 = __importDefault(require("crypto"));
const enterpriseDb_1 = require("../enterpriseDb");
const enterpriseMigration_1 = require("../enterpriseMigration");
const enterpriseAuditService_1 = require("../enterpriseAuditService");
const localSecretStore_1 = require("./localSecretStore");
const DEFAULT_PROVIDER_SCOPE = {
    tenantId: 'default-dev-tenant',
    workspaceId: 'default-workspace',
    userId: 'dev-user-123',
};
const SAFE_PROVIDER_SCOPE_RE = /^[a-zA-Z0-9._:-]+$/;
const SENSITIVE_CONNECTION_FIELDS = [
    'apiKey',
    'claudeApiKey',
    'claudeAuthToken',
    'openaiApiKey',
    'awsBearerToken',
    'awsAccessKeyId',
    'awsSecretAccessKey',
    'awsSessionToken',
];
function enterpriseProviderStoreEnabled() {
    return (0, enterpriseMigration_1.enterpriseDbReadAuthorityEnabled)();
}
function enterpriseProviderDbWritesEnabled() {
    return (0, enterpriseMigration_1.enterpriseDbWritesEnabled)();
}
function legacyProviderWritesEnabled() {
    return (0, enterpriseMigration_1.legacyFilesystemWritesEnabled)();
}
function assertSafeScopeSegment(value, label) {
    if (!SAFE_PROVIDER_SCOPE_RE.test(value) || value === '.' || value === '..') {
        throw new Error(`Unsafe provider ${label}: ${value}`);
    }
    return value;
}
function resolveProviderScope(scope) {
    const rawUserId = scope === undefined ? DEFAULT_PROVIDER_SCOPE.userId : scope.userId;
    return {
        tenantId: assertSafeScopeSegment(scope?.tenantId || DEFAULT_PROVIDER_SCOPE.tenantId, 'tenant id'),
        workspaceId: assertSafeScopeSegment(scope?.workspaceId || DEFAULT_PROVIDER_SCOPE.workspaceId, 'workspace id'),
        userId: rawUserId === undefined || rawUserId === null
            ? null
            : assertSafeScopeSegment(rawUserId, 'user id'),
    };
}
function parseJsonObject(value) {
    if (!value)
        return {};
    try {
        const parsed = JSON.parse(value);
        return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
            ? parsed
            : {};
    }
    catch {
        return {};
    }
}
function toEpochMs(value) {
    const parsed = Date.parse(value);
    return Number.isFinite(parsed) ? parsed : Date.now();
}
function toIsoString(value) {
    return new Date(value).toISOString();
}
function splitConnectionSecrets(connection) {
    const publicConnection = {};
    const secretConnection = {};
    for (const [key, value] of Object.entries(connection)) {
        if (value === undefined)
            continue;
        if (SENSITIVE_CONNECTION_FIELDS.includes(key)) {
            if (typeof value === 'string' && value.length > 0) {
                secretConnection[key] = value;
            }
        }
        else {
            publicConnection[key] = value;
        }
    }
    return { publicConnection, secretConnection };
}
function mergeConnectionSecrets(publicConnection, secretConnection) {
    return {
        ...(publicConnection ?? {}),
        ...secretConnection,
    };
}
function providerSecretRef(scope, providerId) {
    return `secret:provider:${scope.tenantId}:${scope.workspaceId}:${scope.userId ?? '_workspace'}:${providerId}`;
}
function ensureEnterpriseProviderGraph(scope) {
    const now = Date.now();
    const db = (0, enterpriseDb_1.openEnterpriseDb)();
    try {
        db.prepare(`
      INSERT OR IGNORE INTO organizations (id, name, status, plan, created_at, updated_at)
      VALUES (?, ?, 'active', 'enterprise', ?, ?)
    `).run(scope.tenantId, scope.tenantId, now, now);
        db.prepare(`
      INSERT OR IGNORE INTO workspaces (id, tenant_id, name, created_at, updated_at)
      VALUES (?, ?, ?, ?, ?)
    `).run(scope.workspaceId, scope.tenantId, scope.workspaceId, now, now);
        if (scope.userId) {
            db.prepare(`
        INSERT INTO users (id, tenant_id, email, display_name, idp_subject, created_at, updated_at)
        VALUES (?, ?, ?, ?, ?, ?, ?)
        ON CONFLICT(id) DO UPDATE SET
          email = excluded.email,
          display_name = excluded.display_name,
          updated_at = excluded.updated_at
      `).run(scope.userId, scope.tenantId, `${scope.userId}@provider.local`, scope.userId, `provider:${scope.userId}`, now, now);
        }
    }
    finally {
        db.close();
    }
}
function accessibleProviderWhere() {
    return `
    tenant_id = @tenantId
    AND (
      (scope = 'personal' AND workspace_id = @workspaceId AND owner_user_id = @userId)
      OR (scope = 'workspace' AND workspace_id = @workspaceId AND owner_user_id IS NULL)
      OR (scope = 'org' AND workspace_id IS NULL AND owner_user_id IS NULL)
    )
  `;
}
class ProviderStore {
    constructor(filePath) {
        this.providers = new Map();
        this.filePath = filePath;
    }
    load() {
        if (enterpriseProviderStoreEnabled())
            return;
        this.providers.clear();
        if (!fs.existsSync(this.filePath))
            return;
        try {
            const raw = fs.readFileSync(this.filePath, 'utf-8');
            const arr = JSON.parse(raw);
            for (const p of arr)
                this.providers.set(p.id, p);
        }
        catch (err) {
            console.warn('[ProviderStore] Failed to load providers.json, starting fresh:', err.message);
        }
    }
    getAll(scope) {
        if (enterpriseProviderStoreEnabled()) {
            return this.getAllEnterprise(scope);
        }
        return Array.from(this.providers.values());
    }
    get(id, scope) {
        if (enterpriseProviderStoreEnabled()) {
            return this.getEnterprise(id, scope);
        }
        return this.providers.get(id);
    }
    getActive(scope) {
        for (const p of this.getAll(scope)) {
            if (p.isActive)
                return p;
        }
        return undefined;
    }
    getActivePeer(id, scope) {
        if (enterpriseProviderStoreEnabled()) {
            return this.getActiveEnterprisePeer(id, scope);
        }
        return this.getActive(scope);
    }
    getActiveWriteScope(scope) {
        if (enterpriseProviderStoreEnabled()) {
            return this.getActiveEnterpriseWriteScope(scope);
        }
        return this.getActive(scope);
    }
    set(provider, scope) {
        if (!enterpriseProviderStoreEnabled()) {
            this.providers.set(provider.id, provider);
            this.persist();
        }
        if (enterpriseProviderDbWritesEnabled()) {
            this.setEnterprise(provider, scope);
        }
    }
    delete(id, scope) {
        let deleted = false;
        if (!enterpriseProviderStoreEnabled()) {
            deleted = this.providers.delete(id);
            if (deleted)
                this.persist();
        }
        if (enterpriseProviderDbWritesEnabled()) {
            deleted = this.deleteEnterprise(id, scope) || deleted;
        }
        return deleted;
    }
    rotateSecret(id, scope) {
        if (!enterpriseProviderDbWritesEnabled())
            return undefined;
        return this.rotateEnterpriseSecret(id, scope);
    }
    getSecretStore() {
        if (!this.secretStore) {
            this.secretStore = new localSecretStore_1.LocalEncryptedSecretStore();
        }
        return this.secretStore;
    }
    getAllEnterprise(scope) {
        const resolved = resolveProviderScope(scope);
        const db = (0, enterpriseDb_1.openEnterpriseDb)();
        try {
            const rows = db.prepare(`
        SELECT *
        FROM provider_credentials
        WHERE ${accessibleProviderWhere()}
        ORDER BY
          CASE scope
            WHEN 'personal' THEN 0
            WHEN 'workspace' THEN 1
            ELSE 2
          END,
          updated_at DESC
      `).all(resolved);
            return rows
                .map(row => this.providerFromEnterpriseRow(row))
                .filter((provider) => Boolean(provider));
        }
        finally {
            db.close();
        }
    }
    getEnterprise(id, scope) {
        const resolved = resolveProviderScope(scope);
        const db = (0, enterpriseDb_1.openEnterpriseDb)();
        try {
            const row = db.prepare(`
        SELECT *
        FROM provider_credentials
        WHERE id = @id AND ${accessibleProviderWhere()}
        LIMIT 1
      `).get({ ...resolved, id });
            return row ? this.providerFromEnterpriseRow(row) ?? undefined : undefined;
        }
        finally {
            db.close();
        }
    }
    setEnterprise(provider, scope) {
        const resolved = resolveProviderScope(scope);
        ensureEnterpriseProviderGraph(resolved);
        const existing = this.getEnterpriseRowById(provider.id, resolved);
        const effectiveScope = existing?.scope ?? (resolved.userId ? 'personal' : 'workspace');
        const workspaceId = effectiveScope === 'org' ? null : resolved.workspaceId;
        const ownerUserId = effectiveScope === 'personal' ? resolved.userId : null;
        const secretRef = existing?.secret_ref ?? providerSecretRef(resolved, provider.id);
        const { publicConnection, secretConnection } = splitConnectionSecrets(provider.connection);
        const secretVersion = this.getSecretStore().put(secretRef, secretConnection);
        const policy = {
            category: provider.category,
            isActive: provider.isActive,
            connection: publicConnection,
            ...(provider.tuning ? { tuning: provider.tuning } : {}),
            ...(provider.custom ? { custom: provider.custom } : {}),
            secretVersion,
        };
        const db = (0, enterpriseDb_1.openEnterpriseDb)();
        try {
            db.prepare(`
        INSERT INTO provider_credentials
          (id, tenant_id, workspace_id, owner_user_id, scope, name, type, models_json, secret_ref, policy_json, created_at, updated_at)
        VALUES
          (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        ON CONFLICT(id) DO UPDATE SET
          tenant_id = excluded.tenant_id,
          workspace_id = excluded.workspace_id,
          owner_user_id = excluded.owner_user_id,
          scope = excluded.scope,
          name = excluded.name,
          type = excluded.type,
          models_json = excluded.models_json,
          secret_ref = excluded.secret_ref,
          policy_json = excluded.policy_json,
          updated_at = excluded.updated_at
      `).run(provider.id, resolved.tenantId, workspaceId, ownerUserId, effectiveScope, provider.name, provider.type, JSON.stringify(provider.models), secretRef, JSON.stringify(policy), existing?.created_at ?? toEpochMs(provider.createdAt), toEpochMs(provider.updatedAt));
            this.recordProviderSecretAudit(db, {
                action: existing ? 'provider.secret.write' : 'provider.secret.create',
                row: {
                    id: provider.id,
                    tenant_id: resolved.tenantId,
                    workspace_id: workspaceId,
                    owner_user_id: ownerUserId,
                    secret_ref: secretRef,
                },
                secretVersion,
            });
        }
        finally {
            db.close();
        }
    }
    deleteEnterprise(id, scope) {
        const resolved = resolveProviderScope(scope);
        const row = this.getEnterpriseRowById(id, resolved);
        if (!row)
            return false;
        const db = (0, enterpriseDb_1.openEnterpriseDb)();
        try {
            const result = db.prepare(`
        DELETE FROM provider_credentials
        WHERE id = @id AND ${accessibleProviderWhere()}
      `).run({ ...resolved, id });
            if (result.changes > 0) {
                this.getSecretStore().delete(row.secret_ref);
                this.recordProviderSecretAudit(db, {
                    action: 'provider.secret.delete',
                    row,
                    secretVersion: this.readSecretVersionFromPolicy(row),
                });
            }
            return result.changes > 0;
        }
        finally {
            db.close();
        }
    }
    rotateEnterpriseSecret(id, scope) {
        const resolved = resolveProviderScope(scope);
        const row = this.getEnterpriseRowById(id, resolved);
        if (!row)
            return undefined;
        const secretVersion = this.getSecretStore().rotate(row.secret_ref);
        const policy = {
            ...parseJsonObject(row.policy_json),
            secretVersion,
        };
        const db = (0, enterpriseDb_1.openEnterpriseDb)();
        try {
            db.prepare(`
        UPDATE provider_credentials
        SET policy_json = @policyJson, updated_at = @updatedAt
        WHERE id = @id AND ${accessibleProviderWhere()}
      `).run({
                ...resolved,
                id,
                policyJson: JSON.stringify(policy),
                updatedAt: Date.now(),
            });
            this.recordProviderSecretAudit(db, {
                action: 'provider.secret.rotate',
                row,
                secretVersion,
            });
            return secretVersion;
        }
        finally {
            db.close();
        }
    }
    getActiveEnterprisePeer(id, scope) {
        const resolved = resolveProviderScope(scope);
        const row = this.getEnterpriseRowById(id, resolved);
        if (!row)
            return undefined;
        return this.getActiveEnterpriseInCredentialScope({
            tenantId: row.tenant_id,
            workspaceId: row.workspace_id,
            ownerUserId: row.owner_user_id,
            credentialScope: row.scope,
        });
    }
    getActiveEnterpriseWriteScope(scope) {
        const resolved = resolveProviderScope(scope);
        return this.getActiveEnterpriseInCredentialScope({
            tenantId: resolved.tenantId,
            workspaceId: resolved.workspaceId,
            ownerUserId: resolved.userId,
            credentialScope: resolved.userId ? 'personal' : 'workspace',
        });
    }
    getActiveEnterpriseInCredentialScope(input) {
        const db = (0, enterpriseDb_1.openEnterpriseDb)();
        try {
            const rows = db.prepare(`
        SELECT *
        FROM provider_credentials
        WHERE tenant_id = @tenantId
          AND workspace_id IS @workspaceId
          AND owner_user_id IS @ownerUserId
          AND scope = @credentialScope
        ORDER BY updated_at DESC
      `).all(input);
            for (const row of rows) {
                const provider = this.providerFromEnterpriseRow(row);
                if (provider?.isActive)
                    return provider;
            }
            return undefined;
        }
        finally {
            db.close();
        }
    }
    getEnterpriseRowById(id, scope) {
        const db = (0, enterpriseDb_1.openEnterpriseDb)();
        try {
            return db.prepare(`
        SELECT *
        FROM provider_credentials
        WHERE id = @id AND ${accessibleProviderWhere()}
        LIMIT 1
      `).get({ ...scope, id });
        }
        finally {
            db.close();
        }
    }
    providerFromEnterpriseRow(row) {
        const policy = parseJsonObject(row.policy_json);
        const models = parseJsonObject(row.models_json);
        if (typeof models.primary !== 'string' || typeof models.light !== 'string') {
            return null;
        }
        this.recordProviderSecretAudit(undefined, {
            action: 'provider.secret.read',
            row,
            secretVersion: policy.secretVersion,
        });
        const secretConnection = this.getSecretStore().get(row.secret_ref);
        const connection = mergeConnectionSecrets(policy.connection, secretConnection);
        return {
            id: row.id,
            name: row.name,
            category: policy.category ?? 'custom',
            type: row.type,
            isActive: policy.isActive === true,
            createdAt: toIsoString(row.created_at),
            updatedAt: toIsoString(row.updated_at),
            models: {
                primary: models.primary,
                light: models.light,
                ...(typeof models.subAgent === 'string' ? { subAgent: models.subAgent } : {}),
            },
            connection,
            ...(policy.tuning ? { tuning: policy.tuning } : {}),
            ...(policy.custom ? { custom: policy.custom } : {}),
        };
    }
    persist() {
        if (!legacyProviderWritesEnabled())
            return;
        const dir = path.dirname(this.filePath);
        if (!fs.existsSync(dir))
            fs.mkdirSync(dir, { recursive: true });
        const tmp = `${this.filePath}.tmp`;
        fs.writeFileSync(tmp, JSON.stringify(this.getAll(), null, 2));
        fs.renameSync(tmp, this.filePath);
        try {
            fs.chmodSync(this.filePath, 0o600);
        }
        catch { /* Windows */ }
    }
    readSecretVersionFromPolicy(row) {
        const policy = parseJsonObject(row.policy_json);
        return policy.secretVersion;
    }
    recordProviderSecretAudit(db, input) {
        const record = (targetDb) => {
            (0, enterpriseAuditService_1.recordEnterpriseAuditEvent)(targetDb, {
                tenantId: input.row.tenant_id,
                workspaceId: input.row.workspace_id ?? undefined,
                actorUserId: input.row.owner_user_id ?? undefined,
                action: input.action,
                resourceType: 'provider_secret',
                resourceId: input.row.id,
                metadata: {
                    secretRefHash: hashSecretRef(input.row.secret_ref),
                    secretVersion: input.secretVersion,
                    secretStore: this.getSecretStore().info(),
                },
            });
        };
        try {
            if (db) {
                record(db);
                return;
            }
            const auditDb = (0, enterpriseDb_1.openEnterpriseDb)();
            try {
                record(auditDb);
            }
            finally {
                auditDb.close();
            }
        }
        catch (err) {
            console.warn('[ProviderStore] Failed to record provider secret audit:', err.message);
        }
    }
}
exports.ProviderStore = ProviderStore;
function hashSecretRef(secretRef) {
    return `sha256:${crypto_1.default.createHash('sha256').update(secretRef).digest('hex')}`;
}
//# sourceMappingURL=providerStore.js.map