/**
 * Framework-free per-folder access control helpers.
 *
 * These functions decide what a given user can see or write in the workspace.
 * They take `AuthUser | undefined` directly (not a Fastify request) so they
 * can be reused across transports — HTTP (/api/*), MCP HTTP (/mcp), and
 * MCP stdio all share the same rules.
 *
 * Rule summary:
 * - `user === undefined` means "no identified user". On the **web** this is
 *   API-key / open-mode (full access). On **MCP** the caller site rejects
 *   unidentified requests before reaching these helpers, so every MCP tool
 *   can assume a concrete user and `undefined` is never passed in.
 * - A user can see a folder if they are an explicit member (folder_access row),
 *   or if they are the workspace owner AND the folder has more than one member
 *   (shared folders are visible to owners, solo folders stay private).
 * - A user can write to a folder if they are a folder member (role = admin|editor),
 *   or if they are the workspace owner on a shared folder.
 * - A folder with `personal: true` is only visible/writable to its owner,
 *   regardless of folder_access grants or workspace ownership. Stale grants
 *   (from an older sharing attempt or test setup) cannot leak a user's
 *   private notes to another account.
 * - **Owner admin escalation**: workspace owners can lift the personal-
 *   folder restriction by passing `{ includePrivate: true }` to these
 *   helpers. The flag is enforced as owner-only inside the helpers — non-
 *   owners passing it get no extra access. The owner-only CLI (`clone` /
 *   `pull` / `push`) sends it unconditionally so the owner always sees every
 *   folder; web / API surfaces opt in via the `?include_private=1` query
 *   param on `/api/repos` or the `X-Studiograph-Include-Private: 1` header
 *   for git transport. Used for migrations, audits, and offboarding.
 */
import { isWorkspaceOwner } from './auth-service.js';
/** True if the folder is `personal: true` and the caller is not its owner.
 *  The personal flag is an owner-private invariant: it overrides folder_access
 *  rows and workspace ownership. Unidentified callers (open mode / API key)
 *  are never restricted — `undefined` user means full access. */
function isRestrictedPersonalFolder(user, repoName, workspaceManager) {
    if (!user)
        return false;
    const repo = workspaceManager.getRepoConfig(repoName);
    if (!repo?.personal)
        return false;
    // Missing owner_id on a personal folder would be a config bug; err on
    // the side of deny to avoid leaking content.
    return repo.owner_id === undefined || repo.owner_id !== user.id;
}
/** True if the folder is workspace-wide (the reserved shared asset store).
 *  Every authenticated member gets read + write (upload) without a
 *  folder_access grant. This does NOT grant folder-admin (rename/delete the
 *  folder) or per-asset deletion — those stay owner/uploader-scoped. A
 *  workspace-wide folder must never also be `personal`. */
export function isWorkspaceWide(repoName, workspaceManager) {
    return workspaceManager.getRepoConfig(repoName)?.workspace_wide === true;
}
/**
 * Return the list of repos the user can access (content visibility).
 * Unidentified caller (undefined) → full access, preserves legacy
 * web / API-key behavior.
 */
export function getAccessibleRepos(user, workspaceManager, authService, opts = {}) {
    const allRepos = workspaceManager.getAllRepoConfigs();
    if (!user)
        return allRepos;
    const allowed = new Set(authService.getUserFolders(user.id));
    const isOwner = isWorkspaceOwner(user.role);
    const ownerOverride = isOwner && opts.includePrivate === true;
    return allRepos.filter(r => {
        if (ownerOverride)
            return true;
        // Personal folders are owner-only, regardless of grants.
        if (r.personal && r.owner_id !== user.id)
            return false;
        // Workspace-wide folders (the shared asset store) are visible to every
        // member without a grant. (Mutually exclusive with `personal`.)
        if (r.workspace_wide)
            return true;
        if (allowed.has(r.name))
            return true;
        if (isOwner && authService.getFolderMembers(r.name).length > 1)
            return true;
        return false;
    });
}
/** True if the user may read the given folder's contents. */
export function hasRepoAccess(user, repoName, workspaceManager, authService, opts = {}) {
    if (!user)
        return true;
    const isOwner = isWorkspaceOwner(user.role);
    if (isOwner && opts.includePrivate === true)
        return true;
    if (isRestrictedPersonalFolder(user, repoName, workspaceManager))
        return false;
    if (isWorkspaceWide(repoName, workspaceManager))
        return true;
    if (authService.getUserFolders(user.id).includes(repoName))
        return true;
    if (isOwner && authService.getFolderMembers(repoName).length > 1)
        return true;
    return false;
}
/** True if the user is a folder admin on the given repo, OR a workspace
 *  owner on a shared folder (>1 member). Solo/private folders require
 *  actual folder-admin membership — workspace ownership alone does not
 *  let owners rename, delete, or push members into a private folder. */
export function isFolderAdminOrOwner(user, repoName, workspaceManager, authService) {
    if (!user)
        return true;
    if (isRestrictedPersonalFolder(user, repoName, workspaceManager))
        return false;
    if (authService.isFolderAdmin(user.id, repoName))
        return true;
    if (isWorkspaceOwner(user.role) && authService.getFolderMembers(repoName).length > 1)
        return true;
    return false;
}
/**
 * True if the user may write (create/update/delete entities) in the given folder.
 * Membership grants write access regardless of role (both 'admin' and 'editor'
 * can edit), and workspace owners can write to shared folders.
 */
export function canWriteRepo(user, repoName, workspaceManager, authService) {
    if (!user)
        return true;
    if (isRestrictedPersonalFolder(user, repoName, workspaceManager))
        return false;
    // Workspace-wide store: any member may upload (create/update). Per-asset
    // DELETE is restricted separately at the asset-delete route (uploader +
    // workspace owner) — write access here does not imply delete-anyone's-asset.
    if (isWorkspaceWide(repoName, workspaceManager))
        return true;
    const role = authService.getFolderRole(user.id, repoName);
    if (role === 'admin' || role === 'editor')
        return true;
    if (isWorkspaceOwner(user.role) && authService.getFolderMembers(repoName).length > 1)
        return true;
    return false;
}
/**
 * Build a set of all entity IDs visible to the user across their accessible
 * folders. Returns null when the caller is unidentified (no filtering needed —
 * `filterWikilinks` treats null as "no-op").
 */
export function getAccessibleEntityIds(user, workspaceManager, authService) {
    if (!user)
        return null;
    const repos = getAccessibleRepos(user, workspaceManager, authService);
    const ids = new Set();
    for (const repo of repos) {
        try {
            const graph = workspaceManager.getGraph(repo.name);
            for (const e of graph.search({})) {
                ids.add(e.id);
            }
        }
        catch { /* skip unmounted repos */ }
    }
    return ids;
}
/** Filter a wikilink list to only entities the caller can access. */
export function filterWikilinks(wikilinks, accessibleIds) {
    if (!accessibleIds)
        return wikilinks;
    return wikilinks.filter(id => accessibleIds.has(id));
}
//# sourceMappingURL=access-control.js.map