/**
 * Permissions API routes
 *
 * Per-folder membership management. Only folder admins (member with
 * role='admin') can grant/revoke access to their folder; workspace
 * ownership alone does NOT confer folder-admin rights, so owners can't
 * push users into private folders they aren't members of. The list of
 * folders returned by GET /api/permissions/folders is scoped by
 * `isFolderAdminOrOwner` so workspace owners see shared folders they
 * can oversee, but solo/private folders of other users stay hidden
 * (matching the docs/agents/auth-access.md rule that privacy holds even
 * against the workspace owner).
 */
import { isFolderAdminOrOwner } from '../../services/access-control.js';
/** True if the user is a member-admin on the folder. Workspace ownership
 *  alone does NOT qualify — membership is required. */
function canManageFolder(user, folder, authService) {
    if (!user)
        return true; // API key / open mode
    return authService.isFolderAdmin(user.id, folder);
}
export async function registerPermissionsApiRoutes(fastify, authService, workspaceManager, wsHub) {
    // GET /api/permissions/folders — folders the caller can manage or
    // oversee in the Manage Folders tab, with the members and entry count
    // of each. A folder is visible when the caller is a member-admin on it
    // OR the caller is a workspace owner AND the folder is shared (>1
    // member). Solo/private folders of other users stay hidden — workspace
    // ownership alone does not reveal them. Mutation endpoints below still
    // gate on member-admin, so visibility here does not grant edit rights.
    fastify.get('/api/permissions/folders', async (req, reply) => {
        const user = req.user;
        const allRepos = workspaceManager.getAllRepoConfigs();
        const visible = allRepos.filter(repo => isFolderAdminOrOwner(user, repo.name, workspaceManager, authService));
        const result = visible.map(repo => {
            // `listEntityIds()` just walks the folder's subdirectories — no
            // markdown parse, no zod validation. Previously we called
            // `graph.search({})` here which eagerly loaded every entity in
            // every folder on each modal open, turning workspaces with large
            // folders (800+ entries) into a visible stall.
            let entryCount = 0;
            try {
                entryCount = workspaceManager.getGraph(repo.name).listEntityIds().length;
            }
            catch { /* skip */ }
            return {
                folder: repo.name,
                members: authService.getFolderMembers(repo.name),
                entry_count: entryCount,
            };
        });
        return reply.send(result);
    });
    // POST /api/permissions/grant — add a user to a folder with a role.
    fastify.post('/api/permissions/grant', async (req, reply) => {
        const user = req.user;
        const { userId, folder, role } = req.body ?? {};
        if (!userId || !folder) {
            return reply.status(400).send({ error: 'userId and folder are required' });
        }
        if (!canManageFolder(user, folder, authService)) {
            return reply.status(403).send({ error: 'Folder admin access required' });
        }
        if (role && role !== 'admin' && role !== 'editor') {
            return reply.status(400).send({ error: `Invalid role "${role}" — must be "admin" or "editor"` });
        }
        // Preserve the "≥1 admin" invariant when demoting an existing admin.
        const desired = role ?? 'editor';
        const currentRole = authService.getFolderRole(userId, folder);
        if (currentRole === 'admin' && desired === 'editor') {
            const admins = authService.getFolderAdmins(folder);
            if (admins.length <= 1) {
                return reply.status(400).send({ error: 'Cannot demote the last folder admin. Promote another member first.' });
            }
        }
        authService.grantFolderAccess(userId, folder, desired);
        // Patch the recipient's WS folder cache before the event fires —
        // otherwise canReceive() keeps dropping `entity_change` broadcasts
        // for entities in this folder until they reconnect.
        wsHub.addUserFolder(String(userId), folder);
        wsHub.broadcastToUser(String(userId), {
            type: 'access_granted',
            repo: folder,
            userId: String(userId),
            actor: user?.displayName ?? 'admin',
            timestamp: new Date().toISOString(),
        });
        return reply.send({ ok: true });
    });
    // POST /api/permissions/revoke — remove a user from a folder.
    fastify.post('/api/permissions/revoke', async (req, reply) => {
        const user = req.user;
        const { userId, folder } = req.body ?? {};
        if (!userId || !folder) {
            return reply.status(400).send({ error: 'userId and folder are required' });
        }
        if (!canManageFolder(user, folder, authService)) {
            return reply.status(403).send({ error: 'Folder admin access required' });
        }
        // Preserve the "≥1 admin" invariant on shared folders.
        const targetRole = authService.getFolderRole(userId, folder);
        if (targetRole === 'admin') {
            const admins = authService.getFolderAdmins(folder);
            if (admins.length <= 1) {
                return reply.status(400).send({ error: 'Cannot remove the last folder admin. Promote another member first.' });
            }
        }
        authService.revokeFolderAccess(userId, folder);
        wsHub.removeUserFolder(String(userId), folder);
        wsHub.broadcastToUser(String(userId), {
            type: 'access_revoked',
            repo: folder,
            userId: String(userId),
            actor: user?.displayName ?? 'admin',
            timestamp: new Date().toISOString(),
        });
        return reply.send({ ok: true });
    });
}
//# sourceMappingURL=permissions-api.js.map