"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.addTool = addTool;
exports.removeTool = removeTool;
exports.getTool = getTool;
exports.clearSessionCache = clearSessionCache;
exports._resetRegistryForTest = _resetRegistryForTest;
exports.listTools = listTools;
exports.isSuperUser = isSuperUser;
exports.hasClassLevelVerbs = hasClassLevelVerbs;
exports.canRoleInvokeOperation = canRoleInvokeOperation;
const registry = new Map();
/** Per-session pagination cache. Invalidated when a fresh `tools/list` (no cursor) call recomputes. */
const sessionListCache = new Map();
function addTool(def) {
    if (!def?.name)
        throw new Error('addTool: name is required');
    registry.set(def.name, def);
    // New tool → drop pagination caches; next list call rebuilds.
    sessionListCache.clear();
}
function removeTool(name) {
    const existed = registry.delete(name);
    if (existed)
        sessionListCache.clear();
    return existed;
}
function getTool(name) {
    return registry.get(name);
}
/**
 * Drop the pagination cache entry for a session. Called by
 * `session.deleteSession` so the cache doesn't outlive the session that
 * created it. Without this, every session that ever paged tools/list
 * leaves an orphan entry until the process restarts.
 */
function clearSessionCache(sessionId) {
    sessionListCache.delete(sessionId);
}
/** Test seam: drop all registrations. */
function _resetRegistryForTest() {
    registry.clear();
    sessionListCache.clear();
}
/**
 * Filter the registry to tools the user can see on this profile, then page.
 *
 * Cursor encoding: base64 of `{offset:N}`. Opaque to clients per MCP
 * §server/utilities/pagination. A fresh call (no cursor) recomputes and
 * caches the filtered list; subsequent paged calls reuse the cache so
 * pagination is stable even if the registry mutates between pages.
 */
function listTools(args) {
    const { user, profile, sessionId, cursor, limit } = args;
    if (limit < 1)
        throw new Error('listTools: limit must be >= 1');
    let cached = sessionListCache.get(sessionId);
    let offset = 0;
    if (cursor) {
        offset = decodeCursor(cursor);
        if (!cached) {
            // Cache evicted (likely registry change). Treat as a fresh call from
            // the cursor's offset — best effort. The client may see the next
            // page from a slightly different list, which is acceptable per
            // MCP's eventual-consistency stance on listChanged.
            cached = { tools: computeFilteredList(user, profile) };
            sessionListCache.set(sessionId, cached);
        }
    }
    else {
        cached = { tools: computeFilteredList(user, profile) };
        sessionListCache.set(sessionId, cached);
    }
    const slice = cached.tools.slice(offset, offset + limit);
    const next = offset + slice.length;
    return {
        tools: slice,
        nextCursor: next < cached.tools.length ? encodeCursor(next) : undefined,
    };
}
function computeFilteredList(user, profile) {
    const out = [];
    for (const def of registry.values()) {
        if (def.profile !== profile)
            continue;
        if (!def.visibleTo(user))
            continue;
        out.push({
            name: def.name,
            description: def.description,
            inputSchema: def.inputSchema,
            ...(def.outputSchema ? { outputSchema: def.outputSchema } : {}),
            ...(def.annotations ? { annotations: def.annotations } : {}),
        });
    }
    // Sort for deterministic pagination (insertion order would also work, but
    // the registry can mutate between calls and we want stable cursor offsets).
    out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
    return out;
}
function encodeCursor(offset) {
    return Buffer.from(JSON.stringify({ offset }), 'utf8').toString('base64url');
}
function decodeCursor(cursor) {
    try {
        const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
        const offset = decoded?.offset;
        if (typeof offset !== 'number' || offset < 0 || !Number.isFinite(offset) || !Number.isInteger(offset)) {
            return 0;
        }
        return offset;
    }
    catch {
        return 0;
    }
}
// ─── RBAC + introspection helpers ──────────────────────────────────────────
/** True if any role-level flag grants super-user privilege. */
function isSuperUser(user) {
    return user?.role?.permission?.super_user === true;
}
/**
 * Class-level verb introspection. Mirrors `resources/openApi.ts:149-153` so
 * a class is considered to "have" a verb only if its prototype overrides the
 * base Resource implementation. Note that `post`'s default in Harper's
 * `Resource` is a no-op that ResourceClass implementations frequently
 * override implicitly via `update` — the openApi pattern accounts for that.
 */
function hasClassLevelVerbs(prototype, resourcePrototype) {
    return {
        get: typeof prototype.get === 'function' && prototype.get !== resourcePrototype.get,
        post: (typeof prototype.post === 'function' && prototype.post !== resourcePrototype.post) ||
            typeof prototype.update === 'function',
        put: typeof prototype.put === 'function' && prototype.put !== resourcePrototype.put,
        patch: typeof prototype.patch === 'function' && prototype.patch !== resourcePrototype.patch,
        delete: typeof prototype.delete === 'function' && prototype.delete !== resourcePrototype.delete,
    };
}
/**
 * Role-level operations check for the operations profile. Returns true if
 * the user has the role-level privilege required to invoke `operation`,
 * regardless of any per-call schema/table predicate (those run at tool-call
 * time).
 *
 * The implementation here is intentionally conservative — only flags that
 * grant operations globally short-circuit. Per-operation per-target checks
 * are evaluated at call time by Harper's existing `verifyPerms`.
 */
function canRoleInvokeOperation(user, operation) {
    if (isSuperUser(user))
        return true;
    const perm = user?.role?.permission;
    if (!perm)
        return false;
    if (perm.structure_user && SCHEMA_STRUCTURE_OPERATIONS.has(operation))
        return true;
    if (Array.isArray(perm.operations) && perm.operations.includes(operation))
        return true;
    return false;
}
/**
 * Operations that `structure_user` is permitted to invoke. Pre-seeded with
 * the canonical structure ops so the helper is functional and tests have
 * something to assert against. Adding entries here is the right shape;
 * removing them is not (would silently lock users out).
 */
const SCHEMA_STRUCTURE_OPERATIONS = new Set([
    'create_schema',
    'create_database',
    'drop_schema',
    'drop_database',
    'create_table',
    'drop_table',
    'create_attribute',
    'drop_attribute',
]);
//# sourceMappingURL=toolRegistry.js.map