/**
 * Workspace access service — the single architectural boundary through which
 * routes resolve a graph (or read an entity) for an identified user.
 *
 * Why this exists as a service, not a helper:
 *   The helpers in `access-control.ts` are correct but opt-in — a route that
 *   forgets to call `hasRepoAccess()` before `workspaceManager.getGraph()`
 *   silently bypasses access control. After a security audit flagged this as
 *   an architectural risk, we moved the "give me a graph for repo X as user U"
 *   resolution behind this service so the act of getting a graph IS the
 *   access check. There is no method on this surface that returns a graph
 *   without first running the check; bypassing it requires reaching past
 *   the service to `WorkspaceManager.getGraph()` directly, which is what
 *   review now looks for.
 *
 * Routes should call `graphForRead`/`graphForWrite`/`graphForAdmin` instead
 * of `workspaceManager.getGraph(repo)`. The few internal callers that still
 * need raw graph access (SessionManager, YjsManager, vector indexer, etc.)
 * are not user-facing transports and operate with the workspace's own
 * identity, not a request user — they are explicitly out of scope for this
 * boundary.
 */
import { canWriteRepo, filterWikilinks, getAccessibleEntityIds, getAccessibleRepos, hasRepoAccess, isFolderAdminOrOwner, } from './access-control.js';
/**
 * Thrown when an identified user attempts to resolve a graph or read an
 * entity in a folder they cannot access. The route layer maps `kind` to an
 * HTTP status: `not_found` → 404 (don't leak existence to non-members),
 * `forbidden` → 403 (caller can see the folder but lacks the requested role).
 */
export class AccessDeniedError extends Error {
    kind;
    repoName;
    code = 'ACCESS_DENIED';
    constructor(kind, repoName, message) {
        super(message);
        this.kind = kind;
        this.repoName = repoName;
        this.name = 'AccessDeniedError';
    }
}
export class WorkspaceAccessService {
    workspaceManager;
    authService;
    constructor(workspaceManager, authService) {
        this.workspaceManager = workspaceManager;
        this.authService = authService;
    }
    /**
     * Resolve a graph for read. Identified users without read access get a
     * not_found denial — we deliberately mirror the "folder doesn't exist"
     * 404 path so non-members can't probe folder existence by status code.
     * Unidentified callers (API key / open mode) skip the check, matching
     * legacy web behavior.
     */
    graphForRead(user, repoName) {
        // ENFORCEMENT POINT: every read of repo content goes through here.
        // Removing this check would let any authenticated user fetch entries
        // from any folder, including personal/private folders.
        if (!hasRepoAccess(user, repoName, this.workspaceManager, this.authService)) {
            throw new AccessDeniedError('not_found', repoName, `Folder "${repoName}" not found`);
        }
        return this.workspaceManager.getGraph(repoName);
    }
    /**
     * Resolve a graph for write (create / update / delete entities, move
     * folders, etc.). A caller who can read but not write gets a forbidden
     * denial so the UI can render an actionable role-required message.
     */
    graphForWrite(user, repoName) {
        // ENFORCEMENT POINT: write must be checked separately from read because
        // editor and admin both read but only members with editor/admin role
        // can write. A future refactor that collapses this into graphForRead
        // would silently grant write to read-only collaborators.
        if (!hasRepoAccess(user, repoName, this.workspaceManager, this.authService)) {
            throw new AccessDeniedError('not_found', repoName, `Folder "${repoName}" not found`);
        }
        if (!canWriteRepo(user, repoName, this.workspaceManager, this.authService)) {
            throw new AccessDeniedError('forbidden', repoName, `You need editor or admin role to modify entries in "${repoName}".`);
        }
        return this.workspaceManager.getGraph(repoName);
    }
    /**
     * Resolve a graph for folder-admin operations (delete folder, rename
     * folder, manage members). Workspace owners are admins on shared folders
     * but NOT on private/solo folders — see `isFolderAdminOrOwner` for the
     * full rule. This is enforced separately because some operations (renaming
     * a folder) are catastrophic if a regular editor can perform them.
     */
    graphForAdmin(user, repoName) {
        // ENFORCEMENT POINT: admin-only operations must clear the folder-admin
        // bar in addition to read access. Editors must NOT be able to rename or
        // delete the folder out from under their fellow members.
        if (!hasRepoAccess(user, repoName, this.workspaceManager, this.authService)) {
            throw new AccessDeniedError('not_found', repoName, `Folder "${repoName}" not found`);
        }
        if (!isFolderAdminOrOwner(user, repoName, this.workspaceManager, this.authService)) {
            throw new AccessDeniedError('forbidden', repoName, `Folder admin access required for "${repoName}".`);
        }
        return this.workspaceManager.getGraph(repoName);
    }
    /**
     * Read a single entity through the access boundary. Provided as a
     * convenience so routes that just need an entity body don't have to
     * resolve the graph and call `.get()` themselves — every such call site
     * was a place a future refactor could accidentally bypass enforcement.
     */
    readEntity(user, repoName, entityType, entityId) {
        // ENFORCEMENT POINT: even single-entity reads must go through the read
        // gate. Without this, a caller that has the (repo, type, id) tuple from
        // any source (search results, agent output, log line) could fetch its
        // body bypassing folder access.
        const graph = this.graphForRead(user, repoName);
        return graph.get(entityType, entityId);
    }
    /** List repos the user may read. Wraps the helper for symmetry. */
    listReadableRepos(user) {
        return getAccessibleRepos(user, this.workspaceManager, this.authService);
    }
    /** Set of entity IDs visible to the user across their accessible folders. */
    listAccessibleEntityIds(user) {
        return getAccessibleEntityIds(user, this.workspaceManager, this.authService);
    }
    /** Filter a wikilink list to only entities the caller can access. */
    filterWikilinks(wikilinks, accessibleIds) {
        return filterWikilinks(wikilinks, accessibleIds);
    }
    /** Boolean check — for routes that need a 404/200 branch without a graph. */
    canRead(user, repoName) {
        return hasRepoAccess(user, repoName, this.workspaceManager, this.authService);
    }
    /** Boolean check — for routes that need a 403/200 branch without a graph. */
    canWrite(user, repoName) {
        return canWriteRepo(user, repoName, this.workspaceManager, this.authService);
    }
    /** Boolean check — for routes that need a 403/200 branch without a graph. */
    canAdmin(user, repoName) {
        return isFolderAdminOrOwner(user, repoName, this.workspaceManager, this.authService);
    }
}
/**
 * Map an AccessDeniedError to an HTTP reply. Routes catch the error at the
 * top of each handler and call this — keeps the per-route boilerplate to
 * a single try/catch instead of two checks per access kind.
 */
export function sendAccessDenied(reply, err) {
    const status = err.kind === 'not_found' ? 404 : 403;
    return reply.status(status).send({ error: err.message });
}
//# sourceMappingURL=workspace-access.js.map