import { Context } from 'effect'; import { Effect } from 'effect'; import { Schema } from 'effect'; import { Stream } from 'effect'; import { Subject } from '@voltro/protocol'; export declare type AccessPolicy = ReadonlyArray; /** * One access rule. A rule GRANTS when EVERY present condition is satisfied * (AND). A policy is an array of rules; access is allowed when ANY rule * grants (OR). An empty policy on a private object means owner-only (the * implicit owner check always applies). */ export declare interface AccessRule { /** `subject.id === ref.ownerId`. */ readonly owner?: boolean; /** `subject.metadata.roles` intersects these. */ readonly roles?: ReadonlyArray; /** resolved groups (default `subject.metadata.groups`) intersect these. */ readonly groups?: ReadonlyArray; /** `subject.scopes` intersects these. */ readonly scopes?: ReadonlyArray; /** `subject.tenantId === ref.tenantId`. */ readonly tenant?: boolean; /** subject is any authenticated api-key. */ readonly apiKey?: boolean; /** a correct password was supplied (checked at mint, against passwordHash). */ readonly password?: boolean; /** a named custom guard (`options.guards[name]`) returns true. */ readonly guard?: string; } /** * A lazy stream of bytes — the streaming counterpart to {@link StoredObject}'s * eager `bytes`. Backpressured: chunks are pulled on demand, so a multi-GB blob * never fully materialises in memory. The error channel is {@link StorageError} * so a stream handed to `putStream` carries the same typed failure the rest of * the transport does (callers map their source errors into it). */ export declare type ByteStream = Stream.Stream; /** Input to `StorageService.grant`. */ export declare interface GrantInput { readonly refId: string; readonly principalType: GrantPrincipalType; readonly principalId: string; readonly permission?: GrantPermission; readonly expiresAt?: Date | null; readonly createdBy?: string | null; } export declare type GrantPermission = 'read' | 'write'; export declare type GrantPrincipalType = 'user' | 'group' | 'apiKey'; /** * Filter + page a `StorageService.listRefs` / media-library query. Every * filter is optional and ANDed; omit them all to page the whole (tenant-scoped) * ref set newest-first. */ export declare interface ListRefsInput { /** Scope to one tenant (`null` = the system/global partition). Omit to span * every tenant — reserve that for trusted admin reads, not an end-user * media library. */ readonly tenantId?: string | null; /** Match refs whose `folder` equals this value OR sits UNDER it as a path * prefix (`photos` matches `photos` and `photos/2026`). A trailing slash is * normalized away. */ readonly folder?: string; /** Match refs whose `tags` contain ALL of these (AND). */ readonly tags?: ReadonlyArray; /** Match refs created by this subject (the `ownerId` column). */ readonly ownerId?: string; /** Only originals (`false`, the default — excludes transcode renditions / * posters) or include derivatives too (`true`). */ readonly includeDerived?: boolean; /** Page size. Clamped to `[1, 500]`; default 50. */ readonly limit?: number; /** Rows to skip (offset paging). Default 0. */ readonly offset?: number; } /** One page of a `listRefs` query. `nextOffset` is non-null when another page * exists (i.e. the store returned a full `limit + 1` probe) — pass it back as * the next `offset`. */ export declare interface ListRefsResult { readonly refs: ReadonlyArray; readonly nextOffset: number | null; } /** Result of a provider `put` / `head`. */ export declare interface ObjectMeta { readonly size: number; readonly etag?: string; } /** A ref with the server-only `passwordHash` removed — safe to serialize. */ export declare type PublicStorageRef = Omit; /** Input to `StorageService.put`. */ export declare interface PutInput { readonly bytes: Uint8Array; readonly contentType: string; /** Active org id — scopes the key prefix + the ref row. */ readonly tenantId?: string | null; /** Subject id that owns the object (enables the `owner` access rule). */ readonly ownerId?: string | null; /** Optional logical key suffix (e.g. `avatars//x.png`); else a * content-addressed key is derived from the checksum. */ readonly key?: string; /** `private` (default) or `public`. */ readonly visibility?: StorageVisibility; /** Access rules for a private object (in addition to the owner check). */ readonly access?: AccessPolicy; /** Plaintext password — hashed by the service, never stored as-is. */ readonly password?: string; readonly folder?: string; readonly tags?: ReadonlyArray; readonly alt?: string; readonly caption?: string; /** Media duration in seconds (caller-supplied for video/audio). */ readonly duration?: number; /** Parent ref id — set when storing a derived rendition/poster (internal). */ readonly derivedFrom?: string; /** Derivative label — `'rendition'` / `'poster'` (internal). */ readonly kind?: string; } /** One derived output a `Transcoder` produces from a source asset. Each becomes * a normal storage ref linked back to the original via `derivedFrom`. */ export declare interface Rendition { readonly bytes: Uint8Array; readonly contentType: string; /** Derivative class — e.g. `'rendition'` (a playable variant) or `'poster'`. */ readonly kind: string; /** Human label stored as the ref's `caption` — e.g. `'720p'`, `'webm'`. */ readonly label?: string; } /** * Access to a private object was refused. Distinct from `StorageError` so * handlers can tell "not allowed" (403) from "backend blew up" (5xx) and * the wire-error union carries it typed. */ export declare class StorageAccessDenied extends StorageAccessDenied_base { } declare const StorageAccessDenied_base: Schema.TaggedErrorClass; } & { refId: typeof Schema.String; reason: typeof Schema.String; }>; /** * Storage failure. `transient` drives retry — `true` for 429 / 5xx / * network blips, `false` for 4xx / config / not-found. Surfaced typed so * handlers can `Effect.catchTag('StorageError', …)`. */ export declare class StorageError extends StorageError_base { } declare const StorageError_base: Schema.TaggedErrorClass; } & { provider: typeof Schema.String; message: typeof Schema.String; transient: typeof Schema.Boolean; status: Schema.optional; }>; /** A persisted, explicit share of one object with one principal. */ export declare interface StorageGrant { readonly id: string; readonly refId: string; readonly principalType: GrantPrincipalType; readonly principalId: string; readonly permission: GrantPermission; readonly createdAt: Date; readonly expiresAt: Date | null; readonly createdBy: string | null; } /** Custom guard — resolves whether a subject may access a given ref. */ export declare type StorageGuard = (ctx: { readonly subject: Subject; readonly ref: StorageRef; }) => boolean | Promise | Effect.Effect; /** * A backend transport. Keyed by an opaque object key (the service owns key * derivation + tenant prefixing); knows nothing about refs, tenants, or * access policy. */ export declare interface StorageProvider { /** * Where a FILESYSTEM provider writes. Absent for every other provider. * * It exists so a caller can ask a question this interface otherwise cannot * answer: does what I write here survive the process? A rollback capture * written for a pod that then dies is worthless, and `filesystem` is the one * provider that can be either — a mounted volume or an ephemeral container * directory. A deployment pointed this out: their check for "storage is * configured" passed on a `/tmp` path with no volume behind it. */ readonly root?: string; readonly name: string; /** True when `getUrl` returns a real presigned URL (s3/minio). When * false the service serves bytes via the `/_voltro/storage/:id` * endpoint instead (filesystem/memory). */ readonly presigns: boolean; readonly put: (key: string, bytes: Uint8Array, meta: { readonly contentType: string; readonly public?: boolean; }) => Effect.Effect; readonly get: (key: string) => Effect.Effect; readonly delete: (key: string) => Effect.Effect; /** Presigned GET (s3/minio) for private reads. */ readonly getUrl: (key: string, expiresInSec: number) => Effect.Effect; /** A direct, cacheable URL for a PUBLIC object — virtual-hosted bucket * URL or a configured CDN base. `null` when the provider can't serve * publicly without the app (filesystem/memory) → the service falls * back to the `/_voltro/storage/:id` route. */ readonly publicUrl?: (key: string) => string | null; /** Presigned PUT for direct-to-bucket uploads (s3/minio). Absent on * providers that can't presign uploads. */ readonly getUploadUrl?: (key: string, expiresInSec: number, contentType: string) => Effect.Effect; readonly createMultipartUpload?: (key: string, contentType: string, opts?: { readonly public?: boolean; }) => Effect.Effect<{ readonly uploadId: string; }, StorageError>; readonly presignUploadPart?: (key: string, uploadId: string, partNumber: number, expiresInSec: number) => Effect.Effect; readonly completeMultipartUpload?: (key: string, uploadId: string, parts: ReadonlyArray<{ readonly partNumber: number; readonly etag: string; }>) => Effect.Effect<{ readonly etag?: string; }, StorageError>; readonly abortMultipartUpload?: (key: string, uploadId: string) => Effect.Effect; readonly head: (key: string) => Effect.Effect; /** * Stream an object's bytes in chunks instead of buffering the whole blob * ({@link get}). Present on backends with a real streaming read (filesystem, * s3, azure). OPTIONAL: when a provider omits it, {@link getObjectStream} * transparently falls back to `get` + a chunked in-memory stream — correct * for the small-blob backends (memory / database) that have nothing to * stream from. */ readonly getStream?: (key: string) => Effect.Effect; /** * Consume a {@link ByteStream} and store it via the backend's chunked / * multipart upload, so a large upload never fully materialises in memory. * Present on filesystem (streamed atomic write), s3 (multipart via * `@aws-sdk/lib-storage`), azure (`uploadStream`). OPTIONAL: when absent, * {@link putObjectStream} buffers the stream and calls `put`. `totalSize`, * when the caller knows it (e.g. from an export manifest), lets the backend * report an accurate size without a follow-up HEAD. */ readonly putStream?: (key: string, stream: ByteStream, meta: { readonly contentType: string; readonly public?: boolean; readonly totalSize?: number; }) => Effect.Effect; /** * Read a byte range `[start, endInclusive]` (HTTP Range / fd offset). Enables * seekable + resumable reads. OPTIONAL and best-effort: whole-object resume * already works via content-addressing (skip an asset whose hash is present * at the target), so this is an optimisation, not a correctness requirement. */ readonly getRange?: (key: string, start: number, endInclusive: number) => Effect.Effect; /** Providers that store bytes via the app's DataStore (e.g. `database`) * receive it here once it exists — bound by the plugin at boot. */ readonly bindDataStore?: (store: unknown) => void; } export declare type StorageProviderName = 's3' | 'minio' | 'azure' | 'filesystem' | 'memory' | 'database'; /** * Metadata row for a stored blob — the bytes live in object storage; this * (in `_voltro_storage_refs`) is the DB-side handle. `tenantId` = the * active organization id (D5). `passwordHash` is server-only and is never * serialized to a client (stripped from inspect output + URLs). */ export declare interface StorageRef { readonly id: string; readonly tenantId: string | null; /** subject id that created the object (drives the `owner` rule). */ readonly ownerId: string | null; readonly bucket: string; readonly key: string; readonly contentType: string; readonly size: number; readonly checksum: string; readonly visibility: StorageVisibility; readonly accessPolicy: AccessPolicy | null; /** `salt:scryptHash` of the file password, or null. Server-only. */ readonly passwordHash: string | null; readonly createdAt: Date; /** Pixel width (images). */ readonly width?: number | null; /** Pixel height (images). */ readonly height?: number | null; /** Media duration in seconds (video/audio; populated by an app hook — the * core does not bundle a transcoder). */ readonly duration?: number | null; /** A tiny `data:image/webp;base64,…` LQIP placeholder (images). */ readonly placeholder?: string | null; readonly folder?: string | null; readonly tags?: ReadonlyArray | null; readonly alt?: string | null; readonly caption?: string | null; /** Parent ref id when this ref is a derivative; null on an original upload. */ readonly derivedFrom?: string | null; /** Derivative label — e.g. `'rendition'` / `'poster'`; null on an original. */ readonly kind?: string | null; } /** An upload was rejected by a constraint (size / content-type / magic-byte * mismatch). `reason` is a stable code; `detail` is human-readable. */ export declare class StorageRejected extends StorageRejected_base { } declare const StorageRejected_base: Schema.TaggedErrorClass; } & { /** `'too-large' | 'content-type-not-allowed' | 'content-mismatch'` */ reason: typeof Schema.String; detail: typeof Schema.String; }>; /** Scans bytes before they're stored. Return `{ clean:false, threat }` to * reject the upload (fails with `StorageScanRejected`, nothing is stored). */ export declare type StorageScanner = (input: { readonly bytes: Uint8Array; readonly contentType: string; readonly key?: string; }) => StorageScanResult | Promise | Effect.Effect; /** An upload was rejected by the virus scanner. */ export declare class StorageScanRejected extends StorageScanRejected_base { } declare const StorageScanRejected_base: Schema.TaggedErrorClass; } & { threat: typeof Schema.String; }>; export declare interface StorageScanResult { readonly clean: boolean; /** Threat name when `clean` is false. */ readonly threat?: string; } export declare class StorageService extends StorageService_base { } declare const StorageService_base: Context.TagClass; /** The service handlers consume: `const storage = yield* StorageService`. */ export declare interface StorageServiceShape { readonly put: (input: PutInput) => Effect.Effect; /** Fetch an object's bytes by id. Enforces the tenant guard ONLY — it does * NOT run the access policy / grant check. Use this for trusted server-side * reads where the caller has already authorized the access; to deliver a * PRIVATE object to an end user, go through `mintUrl` (access-checked) and * the serve route, never `get` straight off a client-supplied id. */ readonly get: (id: string, opts?: { readonly tenantId?: string | null; }) => Effect.Effect<{ readonly bytes: Uint8Array; readonly ref: StorageRef; }, StorageError>; /** Direct/presigned URL for an object. For PUBLIC objects this is the * CDN/bucket URL (no app round-trip). * * UNCHECKED for PRIVATE objects: it enforces the tenant guard but NOT the * access policy / grants, and on a presigning provider (s3/minio) returns a * live presigned GET — i.e. a working, ungated download URL. To hand a * private object to an end user, use `mintUrl` (it runs `checkAccess` * first). Reach for `getUrl` on a private ref only when the caller has * already authorized the access itself. */ readonly getUrl: (id: string, opts?: { readonly tenantId?: string | null; readonly expiresInSec?: number; }) => Effect.Effect; readonly head: (id: string, opts?: { readonly tenantId?: string | null; }) => Effect.Effect; /** Read a byte range `[start, endInclusive]` of an object (HTTP Range / video * seeking / memory-bounded delivery). Uses the provider's native `getRange` * when present (filesystem / s3 / azure) and otherwise falls back to a * buffered `get` + slice (memory / database — small blobs, nothing to seek). * Clamps `endInclusive` to the object's last byte. Enforces the tenant guard * ONLY, like `get` — the serve route runs the access check before calling it. * Fails `StorageError` status 416 for a start past the end of the object. */ readonly getRange: (id: string, range: { readonly start: number; readonly endInclusive: number; }, opts?: { readonly tenantId?: string | null; }) => Effect.Effect<{ readonly bytes: Uint8Array; readonly ref: StorageRef; readonly totalSize: number; }, StorageError>; readonly delete: (id: string, opts?: { readonly tenantId?: string | null; }) => Effect.Effect; /** Run the access check for `subject`, then return a URL that delivers * the bytes: a public/CDN URL, a presigned GET (s3/minio), or an app * serve URL carrying a short-lived signed grant token (fs/memory). */ readonly mintUrl: (id: string, subject: Subject, opts?: { readonly password?: string; readonly expiresInSec?: number; }) => Effect.Effect; /** Presigned PUT for direct-to-bucket upload (s3/minio). */ readonly mintUploadUrl: (input: { readonly key: string; readonly contentType: string; readonly tenantId?: string | null; readonly expiresInSec?: number; }) => Effect.Effect; /** Whether `subject` may access `ref` (policy + persisted grants). */ readonly checkAccess: (ref: StorageRef, subject: Subject, opts?: { readonly password?: string; readonly permission?: GrantPermission; }) => Effect.Effect; /** List / browse / search the stored refs by `folder` prefix, `tags`, * `ownerId`, and `tenantId`, newest-first, with offset paging. The * first-class media-library / file-manager read — a deployment never has to * query `_voltro_storage_refs` by hand. Enforces NO per-ref access policy * (it filters the index, it doesn't deliver bytes); scope it to the * caller's `tenantId` / `ownerId` for an end-user surface, and deliver any * listed private object through `mintUrl`. */ readonly listRefs: (input?: ListRefsInput) => Effect.Effect; readonly grant: (input: GrantInput) => Effect.Effect; readonly revoke: (grantId: string) => Effect.Effect; /** One grant by id, or null. Server-side read — the RPC layer uses it to * resolve `revoke(grantId)` back to the object whose owner it must check. */ readonly getGrant: (grantId: string) => Effect.Effect; readonly listGrants: (refId: string) => Effect.Effect, StorageError>; /** Bytes + object count for a tenant (drives per-tenant quotas + dashboards). */ readonly usage: (tenantId: string | null) => Effect.Effect<{ readonly bytes: number; readonly count: number; }, StorageError>; /** Garbage-collect dangling refs whose bytes are gone from the provider * (crash between put + insert, or a manual blob delete). Scans up to * `limit` recent refs; run repeatedly for a full sweep. `dryRun` reports * without deleting. */ readonly sweepOrphans: (opts?: { readonly limit?: number; readonly dryRun?: boolean; }) => Effect.Effect<{ readonly scanned: number; readonly removed: ReadonlyArray; }, StorageError>; /** Fetch an external URL SERVER-SIDE and store it as an asset (one-liner CMS * migration / "adopt this remote image"). The content-type comes from the * response unless overridden. NOTE: this fetches an arbitrary URL from the * server — pass only trusted URLs (SSRF); front it with an allow-list if the * URL is user-supplied. */ readonly ingestUrl: (url: string, opts?: { readonly contentType?: string; readonly tenantId?: string | null; readonly ownerId?: string | null; readonly visibility?: StorageVisibility; readonly key?: string; readonly folder?: string; readonly tags?: ReadonlyArray; readonly alt?: string; readonly caption?: string; }) => Effect.Effect; /** Mint a presigned direct-to-bucket PUT for a fresh random TEMP key * (`_incoming/…`). Returns the URL to PUT raw bytes to + the derived key to * hand back to `finalizeUpload`. Only for presigning providers (s3/minio); * fails otherwise so the caller can fall back to the through-app route. */ readonly mintPresignedUpload: (input: { readonly contentType: string; readonly tenantId?: string | null; readonly expiresInSec?: number; }) => Effect.Effect<{ readonly uploadUrl: string; readonly key: string; readonly expiresInSec: number; }, StorageError>; /** Promote a blob the client PUT directly to a TEMP `key` into a real ref: * fetch it back, run the FULL `put()` pipeline (scan/checksum/derivatives/ * quota), then delete the temp key. This is where a presigned upload gets * scanned + a non-degraded ref — out of band from the direct PUT. On a scan * reject the temp bytes are deleted (fail-closed, nothing promoted). */ readonly finalizeUpload: (input: { readonly key: string; readonly contentType: string; readonly tenantId?: string | null; readonly ownerId?: string | null; readonly visibility?: StorageVisibility; readonly folder?: string; readonly tags?: ReadonlyArray; readonly alt?: string; readonly caption?: string; }) => Effect.Effect; /** Store one chunk of a resumable upload as its own durable `_incoming// * ` object. Idempotent — re-PUTting the same index overwrites it. */ readonly putResumableChunk: (input: { readonly uploadId: string; readonly index: number; readonly bytes: Uint8Array; readonly tenantId?: string | null; }) => Effect.Effect; /** How far a resumable upload got: probes contiguous chunk objects from index * 0 and returns the received byte `offset` + the `nextIndex` to send. Lets a * reconnecting client resume instead of restarting. */ readonly resumableStatus: (input: { readonly uploadId: string; readonly tenantId?: string | null; }) => Effect.Effect<{ readonly offset: number; readonly nextIndex: number; }, StorageError>; /** Assemble a completed resumable upload: read chunks `0..count-1` in order, * concatenate, run the FULL `put()` pipeline, then delete the chunk objects * (on success AND on reject — fail-closed). */ readonly finalizeResumable: (input: { readonly uploadId: string; readonly count: number; readonly contentType: string; readonly tenantId?: string | null; readonly ownerId?: string | null; readonly visibility?: StorageVisibility; readonly folder?: string; readonly tags?: ReadonlyArray; readonly alt?: string; readonly caption?: string; }) => Effect.Effect; /** Run the configured transcoder over a stored asset and persist each output * as a rendition ref (linked via `derivedFrom`). Idempotent-ish: re-running * content-addresses identical outputs. No-op (→ `[]`) when no transcoder is * configured or the ref is itself a derivative. */ readonly transcode: (refId: string, opts?: { readonly tenantId?: string | null; }) => Effect.Effect, StorageError | StorageRejected | StorageScanRejected>; /** List the derivative refs (renditions, posters) produced from `refId`. */ readonly renditions: (refId: string, opts?: { readonly tenantId?: string | null; }) => Effect.Effect, StorageError>; /** Begin a multipart session on a presigning provider (s3/minio). Returns the * bucket `key` + provider `uploadId` to sign parts against. Fails when the * provider can't do multipart (the client falls back to `resumable`). */ readonly beginMultipartUpload: (input: { readonly contentType: string; readonly tenantId?: string | null; readonly visibility?: StorageVisibility; readonly expiresInSec?: number; }) => Effect.Effect<{ readonly uploadId: string; readonly key: string; }, StorageError>; /** Presign a PUT for one part (1-based `partNumber`). */ readonly signMultipartPart: (input: { readonly key: string; readonly uploadId: string; readonly partNumber: number; readonly expiresInSec?: number; }) => Effect.Effect; /** Complete a multipart session → the bucket assembles the object; a ref is * registered straight from it (size via HEAD, etag as checksum). No fetch-back, * so NO scan/derivatives — scan multi-GB out of band. */ readonly completeMultipart: (input: { readonly key: string; readonly uploadId: string; readonly parts: ReadonlyArray<{ readonly partNumber: number; readonly etag: string; }>; readonly contentType: string; readonly tenantId?: string | null; readonly ownerId?: string | null; readonly visibility?: StorageVisibility; readonly folder?: string; readonly tags?: ReadonlyArray; readonly alt?: string; readonly caption?: string; }) => Effect.Effect; /** Abort a multipart session (client cancelled) — frees the bucket's parts. */ readonly abortMultipart: (input: { readonly key: string; readonly uploadId: string; }) => Effect.Effect; } /** `public` = anyone, served direct from bucket/CDN; `private` = gated by * the ref's access policy + grants. Default for a new put is `private`. */ export declare type StorageVisibility = 'public' | 'private'; /** Bytes + content type as returned by a provider `get`. */ export declare interface StoredObject { readonly bytes: Uint8Array; readonly contentType: string; } /** A streamed read from a provider — the object as a {@link ByteStream} plus * its content type and, when the backend reports it up front, its size. */ export declare interface StoredStream { readonly stream: ByteStream; readonly contentType: string; readonly size?: number; } /** * App-supplied (or first-party `ffmpegTranscoder`) transcode step. Given a * source asset's bytes, returns zero or more renditions (720p mp4, webm, HLS, * a poster, …). The core bundles no transcoder — wire ffmpeg (or a cloud API). * Runs OUT OF BAND (never blocks `put()`); best-effort — a throw/failure/[] just * means no renditions. */ export declare type Transcoder = (input: { readonly bytes: Uint8Array; readonly contentType: string; readonly ref: StorageRef; }) => ReadonlyArray | Promise> | Effect.Effect>; export declare interface UploadLimits { /** Reject uploads larger than this many bytes. */ readonly maxBytes?: number; /** Allowed content types — exact (`application/pdf`) or wildcard * (`image/*`). Omit to allow any. */ readonly allowedContentTypes?: ReadonlyArray; /** Verify the bytes' magic number matches the declared `contentType` * (blocks e.g. an executable uploaded as `image/png`). Default false. */ readonly sniff?: boolean; } /** * App-supplied media probe for video/audio uploads. The core deliberately does * NOT bundle ffmpeg/ffprobe (a large binary + licensing) — wire your own * (ffprobe, a cloud video API) and the service calls it in `put()` for * video/audio content types. Best-effort: a null/throw never blocks the upload. */ export declare type VideoProbe = (input: { readonly bytes: Uint8Array; readonly contentType: string; }) => VideoProbeResult | null | Promise | Effect.Effect; /** What a `VideoProbe` returns for a video/audio upload. All optional — return * `null` (or throw) and the upload proceeds with no media metadata. */ export declare interface VideoProbeResult { /** Duration in seconds. */ readonly duration?: number; readonly width?: number; readonly height?: number; /** A poster frame as a `data:image/…;base64,…` URI — stored as the ref's * `placeholder` (so a `