import { DriveBackup } from "../backup/drive.js"; import { SyncOutbox } from "../sync/outbox.js"; import { OutboxWorker } from "../sync/worker.js"; export interface WikiFile { fileId: string; name: string; content: string; revision: string; modifiedTime: string; } export interface WriteResult { fileId: string; revision: string; modifiedTime: string; } export interface FileEntry { fileId: string; name: string; modifiedTime: string; size: number; } export interface StorageClient { listFiles(): Promise; readFile(name: string): Promise; createFile(name: string, content: string): Promise; updateFile(fileId: string, content: string, seenRevision: string): Promise; renameFile(fileId: string, newName: string): Promise; deleteFile(name: string): Promise; bootstrapWikiFiles(): Promise; verifyAccess(): Promise; getStorageLocation(): string; /** * Cheap existence probe. Exists so callers that only need "is this object * there" do not download it — the scoped-client wipe check was pulling the * whole of index.md (~200 KB on a large wiki) every 5 minutes per user just * to answer this question (GH #412). */ fileExists(name: string): Promise; } export declare function contentRevision(content: string): string; export declare class LocalClient implements StorageClient { private readonly wikiDir; private readonly backup; private readonly outbox; private readonly worker; constructor(wikiDir: string, backup?: DriveBackup | null); /** Exposed for the file watcher (external-edit producer) and tests. */ getOutbox(): SyncOutbox | null; /** Exposed for the file watcher (external-edit producer) and tests. */ getOutboxWorker(): OutboxWorker | null; getStorageLocation(): string; verifyAccess(): Promise; fileExists(name: string): Promise; listFiles(): Promise; readFile(name: string): Promise; createFile(name: string, content: string): Promise; updateFile(fileId: string, content: string, seenRevision: string): Promise; renameFile(fileId: string, newName: string): Promise; deleteFile(name: string): Promise; bootstrapWikiFiles(): Promise; private assertSafePath; private scheduleBackup; private pruneEmptyParentDirs; } export declare class SupabaseStorageClient implements StorageClient { private readonly supabaseUrl; private readonly serviceRoleKey; private readonly bucket; private readonly prefix; private readonly cacheLocation; private readonly backup; constructor(options: { supabaseUrl: string; serviceRoleKey: string; bucket: string; prefix?: string; cacheLocation?: string; backup?: DriveBackup | null; }); withPrefix(additionalPrefix: string): SupabaseStorageClient; getStorageLocation(): string; verifyAccess(): Promise; listFiles(): Promise; /** * HEAD the object rather than downloading it. * * Deliberately conservative: only a 404 is treated as "absent". Any other * non-OK status falls back to a full read, because the sole caller uses this * to detect an externally wiped wiki, and a false negative would trigger an * unnecessary re-bootstrap and index backfill. */ fileExists(name: string): Promise; readFile(name: string): Promise; createFile(name: string, content: string): Promise; updateFile(fileId: string, content: string, seenRevision: string): Promise; renameFile(fileId: string, newName: string): Promise; deleteFile(name: string): Promise; bootstrapWikiFiles(): Promise; private authHeaders; private objectPath; /** * Breadth-walks the bucket with at most STORAGE_LIST_CONCURRENCY * `listObjects` calls in flight at any moment, regardless of how many * directories are discovered along the way. * * The previous implementation recursed per subdirectory and fanned every * recursive call out via `Promise.all`, so a bucket with N directories at * any depth put N concurrent list requests in flight simultaneously — * enough to exceed Supabase's connection pool on a large wiki (GH #556). * A plain worker pool over a flat queue can't be used here because the * queue only grows as directories are discovered, so this drives it with * an explicit pending-count instead of a fixed item list. */ private walkList; private listObjects; private uploadObject; private deleteObject; private pruneEmptyParentDirs; private isDirectoryItem; private parseModifiedTime; private cacheKeySegment; private sanitizeForCache; private scheduleBackup; } export declare function getLocalClient(): Promise; export declare function getStorageClient(): Promise; export declare function createScopedStorageClient(baseClient: StorageClient, scopePrefix: string): StorageClient; export interface ProjectGrantRoute { /** Folder-prefix the grant covers, e.g. 'projects/5280' (no trailing slash). */ prefix: string; /** Storage client scoped to the grant owner's own namespace root. */ client: StorageClient; /** Supabase subject of the grant owner — the true owner for index-attribution purposes (BRA-381). */ ownerSubject: string; /** * Grant level (GH #798 / BRA-5160). Omitted for rows written before this * column was enforced — those must keep behaving as 'read_write', so every * reader treats a missing value as 'read_write', never as a stricter default. */ permissions?: "read_write" | "read_only"; } export declare class GrantAwareStorageClient implements StorageClient { private readonly ownClient; private readonly grants; private readonly ownSubject; constructor(ownClient: StorageClient, grants: ProjectGrantRoute[], ownSubject: string); private findGrant; private resolve; /** * Throws when `name` is not writable by the grantee: either the whole grant * is read_only (GH #798 / BRA-5160), or `name` is an owner-only instructions * page reached through an active grant. Grantee writes route here for * create/update/rename/delete — the owner never goes through * GrantAwareStorageClient for their own project (httpServer.ts only wraps a * client in grants the *actor* holds as a grantee), so any caller that lands * here on a granted path is, by construction, not the owner. */ private assertWritable; /** * The true storage-owner subject for `name` — the grant owner's subject if * the path falls under an active grant, otherwise the requester's own * subject. Search-index writes must attribute to this, not the raw * authenticated actor, or an owner never sees a grantee's contributions * via list_pages/search_pages (BRA-381). */ resolveOwnerSubject(name: string): string; getStorageLocation(): string; verifyAccess(): Promise; bootstrapWikiFiles(): Promise; fileExists(name: string): Promise; listFiles(): Promise; /** * Own-namespace files only, excluding anything merged in from grants. * audit_index compares this listing against `listAllPageNames(userId)`, * which likewise only returns the actor's own index rows — the merged * `listFiles()` would put grant-owner files on the Drive side with no * counterpart on the index side, fabricating "unindexed" drift for shared * pages the actor never actually owns (BRA-1857 / GH #453). */ listOwnFiles(): Promise; readFile(name: string): Promise; createFile(name: string, content: string): Promise; updateFile(fileId: string, content: string, seenRevision: string): Promise; renameFile(fileId: string, newName: string): Promise; deleteFile(name: string): Promise; } /** * Resolves the correct subject to attribute a Postgres search-index write to * for `name`: the grant owner's subject when `drive` is grant-aware and * `name` falls under an active grant, otherwise `actorUserId` unchanged. * Use this — not the raw authenticated actor — for every index call * (upsertPage, deletePage, fetchPageRow, syncPageLinks, etc.) so an owner can * see a grantee's contributions via list_pages/search_pages (BRA-381). */ export declare function resolveIndexOwnerSubject(drive: StorageClient, name: string, actorUserId: string): string; //# sourceMappingURL=client.d.ts.map