/**
 * 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 type { WorkspaceManager } from '../core/workspace-manager.js';
import type { BaseGraphManager, EntityFile } from '../core/graph.js';
import type { EntityType } from '../core/validation.js';
import type { AuthService, AuthUser } from './auth-service.js';
export type AccessMode = 'read' | 'write' | 'admin';
/**
 * 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 declare class AccessDeniedError extends Error {
    readonly kind: 'not_found' | 'forbidden';
    readonly repoName: string;
    readonly code: "ACCESS_DENIED";
    constructor(kind: 'not_found' | 'forbidden', repoName: string, message: string);
}
export declare class WorkspaceAccessService {
    private readonly workspaceManager;
    private readonly authService;
    constructor(workspaceManager: WorkspaceManager, 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: AuthUser | undefined, repoName: string): BaseGraphManager;
    /**
     * 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: AuthUser | undefined, repoName: string): BaseGraphManager;
    /**
     * 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: AuthUser | undefined, repoName: string): BaseGraphManager;
    /**
     * 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: AuthUser | undefined, repoName: string, entityType: EntityType, entityId: string): EntityFile;
    /** List repos the user may read. Wraps the helper for symmetry. */
    listReadableRepos(user: AuthUser | undefined): {
        name: string;
        path: string;
        display_name?: string | undefined;
        type?: string | undefined;
        description?: string | undefined;
        personal?: boolean | undefined;
        owner_id?: number | undefined;
        system?: boolean | undefined;
        workspace_wide?: boolean | undefined;
        sync?: {
            source: string;
            entity_type?: string | undefined;
            filter?: Record<string, any> | undefined;
        }[] | undefined;
    }[];
    /** Set of entity IDs visible to the user across their accessible folders. */
    listAccessibleEntityIds(user: AuthUser | undefined): Set<string> | null;
    /** Filter a wikilink list to only entities the caller can access. */
    filterWikilinks(wikilinks: string[], accessibleIds: Set<string> | null): string[];
    /** Boolean check — for routes that need a 404/200 branch without a graph. */
    canRead(user: AuthUser | undefined, repoName: string): boolean;
    /** Boolean check — for routes that need a 403/200 branch without a graph. */
    canWrite(user: AuthUser | undefined, repoName: string): boolean;
    /** Boolean check — for routes that need a 403/200 branch without a graph. */
    canAdmin(user: AuthUser | undefined, repoName: string): boolean;
}
/**
 * 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 declare function sendAccessDenied(reply: {
    status: (n: number) => any;
}, err: AccessDeniedError): any;
