/** * Access-control helpers for shareable resources. * * The access model combines: * 1. Direct ownership — `owner_email = currentUser`. * 2. Visibility — `'private' | 'org' | 'public'`. `org` grants read to anyone * in the same org; `public` grants read to any authenticated user. * 3. Share rows — per-user or per-org grants in the `{type}_shares` table * with a role (`viewer | editor | admin`). * * Use `applyAccessFilter()` on list/read queries to filter rows the current * user can see. Use `assertAccess()` at the top of write actions to reject * callers who lack the required role. */ import { type SQL } from "drizzle-orm"; import { type ShareableResourceRegistration } from "./registry.js"; import { type ShareRole, type Visibility } from "./schema.js"; export declare class ForbiddenError extends Error { statusCode: number; constructor(message?: string); } export interface AccessContext { userEmail?: string; orgId?: string; } /** Current request's access context. Pulls from request-context ALS. */ export declare function currentAccess(): AccessContext; export declare function resolveRegisteredAccessContext(reg: ShareableResourceRegistration | undefined, ctx: AccessContext): AccessContext; /** * Build a Drizzle `WHERE` clause that admits rows the current user can see. * Pass the ownable resource table and its shares table; optional min role * (defaults to 'viewer') gates which share rows count. * * `visibility = 'public'` is intentionally NOT admitted by default. Public * means "anyone with the link can view" (still honoured by `resolveAccess` * for read-by-id), not "appears in every signed-in user's list/sidebar." * Pass `{ includePublic: true }` for the rare list endpoint that wants * cross-user public discovery (a public template gallery, for example). * * Example: * * const rows = await db * .select() * .from(schema.documents) * .where(accessFilter(schema.documents, schema.documentShares)); */ export declare function accessFilter(resourceTable: any, sharesTable: any, rawCtx?: AccessContext, minRole?: ShareRole, options?: { includePublic?: boolean; }): SQL; export interface ResolvedAccess { /** Effective role: 'owner' for the resource owner, or the share role. */ role: "owner" | ShareRole; /** The resource row (already loaded). */ resource: any; } /** * Minimal resource shape returned when a caller opts into a projected access * load via `{ skipResourceBody: true }`. Contains exactly the columns the * access-decision logic itself reads — identity, ownership, org scope, and * visibility — never a resource type's heavy body columns (`data`, * `content`, and similar blobs). */ export interface AccessProjectedResource { id: string; ownerEmail: string; orgId: string | null; visibility: Visibility; } export interface ResolvedAccessProjected { /** Effective role: 'owner' for the resource owner, or the share role. */ role: "owner" | ShareRole; /** Only the access-decision columns — not the full resource row. */ resource: AccessProjectedResource; } export interface ResolveAccessOptions { /** * When true, load only the columns the access decision itself needs * (`id`, `ownerEmail`, `orgId`, `visibility`) instead of the full resource * row. Use this when the caller only needs the access decision — not the * resource body — and wants to skip fetching heavy type-specific columns * (e.g. `data`/`content` blobs) on every `assertAccess`/`resolveAccess` * call. * * Silently ignored (falls back to a full row load) for any resource type * registered with a `publicAccessRole` *function* resolver, since that * callback can read arbitrary resource fields the projection would have * omitted — see `hasDynamicPublicAccessRoleResolver` below. * * Default: `false` — the full row is loaded, matching historical behavior. * Callers that need resource body fields (most call sites today — for * many resource types `resolveAccess().resource` doubles as the action's * primary data read) must NOT pass this. */ skipResourceBody?: boolean; } /** * Return the effective role the current user has on a specific resource, or * null if they have no access. Loads the resource and relevant share rows. * * By default the full resource row is loaded (unchanged historical * behavior — for most resource types `.resource` here doubles as the * action's primary data read). Pass `{ skipResourceBody: true }` when the * caller only needs the access decision to load just the access-decision * columns instead — see `ResolveAccessOptions`. */ export declare function resolveAccess(resourceType: string, resourceId: string, rawCtx?: AccessContext, options?: { skipResourceBody?: false; }): Promise; export declare function resolveAccess(resourceType: string, resourceId: string, rawCtx: AccessContext | undefined, options: { skipResourceBody: true; }): Promise; /** * Throw ForbiddenError if the current user can't act on this resource with at * least the given role. Used at the top of update/delete actions. * * By default the full resource row is loaded (unchanged historical * behavior). Pass `{ skipResourceBody: true }` as the fifth argument when * the caller only needs the access decision — see `ResolveAccessOptions`. */ export declare function assertAccess(resourceType: string, resourceId: string, minRole?: ShareRole | "owner", ctx?: AccessContext, options?: { skipResourceBody?: false; }): Promise; export declare function assertAccess(resourceType: string, resourceId: string, minRole: ShareRole | "owner" | undefined, ctx: AccessContext | undefined, options: { skipResourceBody: true; }): Promise; //# sourceMappingURL=access.d.ts.map