import { ColumnBuilder } from '@voltro/database'; import { ColumnType } from '@voltro/database'; import { Context } from 'effect'; import { DataStore } from '@voltro/database'; import { Effect } from 'effect'; import { Layer } from 'effect'; import { mixin } from '@voltro/database'; import { Readable } from 'node:stream'; import { Schema } from 'effect'; import { Stream } from 'effect'; import { Subject } from '@voltro/protocol'; import { TableLike } from '@voltro/database'; import { VoltroPlugin } from '@voltro/protocol'; export declare interface AccessOptions { /** Resolve a subject's group ids. Default: `subject.metadata.groups`. */ readonly resolveGroups?: (subject: Subject) => ReadonlyArray | Promise>; /** Named custom guards referenced by `rule.guard`. */ readonly guards?: Record; } 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; } /** * The provider the APP configured, falling back to the env-derived default when no * `storagePlugin(...)` is installed. Use this — not `resolveStorageProvider({})` — * anywhere outside the plugin that needs "the storage this app actually uses". */ export declare const appStorageProvider: () => StorageProvider; /** A column binding an entity to a stored asset (a `_voltro_storage_refs` id). */ export declare function assetRef(options: AssetRefOptions & { readonly nullable: false; }): ColumnBuilder; export declare function assetRef(options?: AssetRefOptions): ColumnBuilder; export declare interface AssetRefOptions { /** Enforce a DB foreign key to `_voltro_storage_refs.id`. Default true. */ readonly fk?: boolean; /** FK onDelete when `fk` is true. Default `'setNull'`. */ readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction'; /** Nullable? Default true — an entity may have no asset yet. */ readonly nullable?: boolean; } export declare const azureProvider: (options: AzureProviderOptions) => StorageProvider; export declare interface AzureProviderOptions { /** Container name (the "bucket"). */ readonly container: string; /** Storage account name (for the default `https://.blob.core.windows.net` host + SAS). */ readonly accountName?: string; /** Account key — required to mint SAS URLs (presigned get/put). */ readonly accountKey?: string; /** Full connection string (alternative to accountName/accountKey for the client; SAS still needs the key). */ readonly connectionString?: string; /** Custom blob endpoint (e.g. Azurite `http://127.0.0.1:10000/devstoreaccount1`). */ readonly endpoint?: string; /** Public base URL for public objects (a CDN / public container host). */ readonly cdnBaseUrl?: string; } export declare const buildStorageService: (options: StorageServiceOptions) => StorageServiceShape; /** Slice a buffer into a lazy chunked ByteStream. Backs the buffered fallback * + the in-memory/DB providers that have a whole buffer, not a backend * stream. `subarray` keeps the chunks as views (no copy). */ export declare const bytesToStream: (bytes: Uint8Array, chunkSize?: number) => ByteStream; /** * 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; /** Check an upload against the limits. Returns `null` when ok, else a * `{ reason, detail }` describing the violation. */ export declare const checkLimits: (bytes: Uint8Array, contentType: string, limits: UploadLimits | undefined) => { readonly reason: string; readonly detail: string; } | null; export declare interface ClamavOptions { /** clamd host. Default `127.0.0.1` (env `CLAMAV_HOST`). */ readonly host?: string; /** clamd port. Default `3310` (env `CLAMAV_PORT`). */ readonly port?: number; /** Abort + reject after this many ms. Default 30_000. */ readonly timeoutMs?: number; /** INSTREAM chunk size. Default 64 KiB. */ readonly chunkSize?: number; } /** * A `StorageScanner` backed by a running clamd. Wire it via * `storagePlugin({ scan: clamavScanner({ host, port }) })`. A scanner error * (clamd unreachable) surfaces as a transient `StorageError` from `put` — * fail-closed, so a misconfigured scanner doesn't silently wave files * through. An actual detection fails with `StorageScanRejected`. */ export declare const clamavScanner: (options?: ClamavOptions) => StorageScanner; /* Excluded from this release type: clearStorageBuffer */ /** Drain a ByteStream into a single Uint8Array (buffered fallback / small * blobs). Defeats the memory win — only for backends that can't stream. */ export declare const collectStream: (stream: ByteStream) => Effect.Effect; /** Concatenate byte chunks into one contiguous Uint8Array. */ export declare const concatBytes: (chunks: ReadonlyArray) => Uint8Array; /** * The app's provider, or `undefined` when NOBODY named one. * * `resolveProvider({})` cannot answer this: its final `default:` arm returns an * in-process `memoryProvider`, so it is total by construction and every caller * checking `if (!provider)` is checking a condition that cannot hold. Three of * them existed — the `--assets` refusals in `voltro data backup` / `export` / * `restore`, whose whole job is to refuse a flag the app cannot honour. They * were dead, and what they let through is worse than an unchecked flag: with no * storage configured, `backup --assets` captured from a FRESH memory provider, * found nothing in it, and wrote an artefact stamped `assets: { count: 0 }` — * a rollback story that says the blobs are in there. * * "Configured" means one of the two things a person actually did: installed * `storagePlugin(...)` (which registers here at construction), or set * `STORAGE_PROVIDER`. A memory provider nobody asked for is not a decision. */ export declare const configuredStorageProvider: () => StorageProvider | undefined; /** `image/*` matches `image/png`; exact otherwise. */ export declare const contentTypeAllowed: (contentType: string, allowed: ReadonlyArray | undefined) => boolean; /** * Stores blob bytes IN the app's database (`_voltro_storage_blobs`) via the * bound DataStore — no external object store. The plugin wires the DataStore * through `bindDataStore` once it exists (the provider can't be used before * that). For small files; large blobs belong in object storage. */ export declare const databaseProvider: () => StorageProvider & { bindDataStore: (store: unknown) => void; }; export declare const dataStoreGrantStore: (store: DataStore) => GrantStore; /** Ref store backed by the app's `DataStore` (`_voltro_storage_refs`). */ export declare const dataStoreRefStore: (store: DataStore) => RefStore; /** Decode a `data:;base64,` URI to bytes + content type. */ export declare const decodeDataUri: (dataUri: string) => { bytes: Uint8Array; contentType: string; }; /** * What to tell an operator about a capture written to `path`. * * Says what was OBSERVED and what follows, and stops there. "Your storage is * ephemeral" is a claim about their cluster; "this path is on the same * filesystem as `/`" is a fact about this process. */ export declare const durabilityNotice: (path: string) => string | undefined; /** Frame bytes for clamd `INSTREAM`: `zINSTREAM\0` then `` * chunks, terminated by a zero-length chunk. Exposed for testing. */ export declare const encodeInstream: (bytes: Uint8Array, chunkSize?: number) => Buffer; export declare interface FfmpegRenditionSpec { /** Derivative class stored on the rendition ref — `'rendition'` / `'poster'`. */ readonly kind: string; /** Human label stored as the rendition's caption — `'720p'`, `'webm'`. */ readonly label?: string; /** Output content type — `'video/mp4'`, `'video/webm'`, `'image/jpeg'`. */ readonly contentType: string; /** Output file extension, no dot — `'mp4'`, `'webm'`, `'jpg'`. */ readonly ext: string; /** ffmpeg OUTPUT args (between `-i ` and the output file). e.g. * `['-vf','scale=-2:720','-c:v','libx264','-crf','23','-c:a','aac']`. */ readonly args: ReadonlyArray; } /** A {@link Transcoder} backed by the ffmpeg binary — see the file header. */ export declare const ffmpegTranscoder: (opts: FfmpegTranscoderOptions) => Transcoder; export declare interface FfmpegTranscoderOptions { readonly renditions: ReadonlyArray; /** Path to the ffmpeg binary. Default `'ffmpeg'` (resolved on PATH). */ readonly ffmpegPath?: string; /** Per-rendition timeout in ms. Default 5 minutes. */ readonly timeoutMs?: number; } export declare const filesystemProvider: (options: FilesystemProviderOptions) => StorageProvider; export declare interface FilesystemProviderOptions { readonly root: string; } /** What a finalize token authorizes: promoting ONE temp-key blob into a real * ref under the bound identity, until `exp`. */ export declare interface FinalizeTicketPayload extends Omit { /** The TEMP bucket key the client PUT its bytes to (required — finalize reads * exactly this key, so it can't be aimed at another object). */ readonly key: string; } /** * A Node `Readable` (yields Buffer/Uint8Array) → ByteStream. `make` is called * lazily on first pull so the fd / HTTP body isn't opened until consumption * starts. Read failures surface as a transient {@link StorageError}. */ export declare const fromNodeReadable: (provider: string, make: () => Readable) => ByteStream; /** * Read an object as a stream, using the provider's `getStream` when it has one * and otherwise buffering via `get`. The export layer calls this; it never has * to know whether a given backend streams. */ export declare const getObjectStream: (provider: StorageProvider, key: string) => Effect.Effect; /** 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'; export declare interface GrantStore { readonly insert: (grant: StorageGrant) => Effect.Effect; readonly delete: (id: string) => Effect.Effect; /** One grant by id, or null. Needed so `storage.revoke` — which is handed a * GRANT id, not a ref id — can find the object whose owner it must check. */ readonly getById: (id: string) => Effect.Effect; readonly listByRef: (refId: string) => Effect.Effect, StorageError>; readonly deleteByRef: (refId: string) => Effect.Effect; } /** What a grant token authorizes: read access to one ref until `exp`. */ export declare interface GrantTokenPayload { readonly refId: string; /** Unix seconds when the token expires. */ readonly exp: number; } /** Hash a file password as `salt:scryptHash` (hex). */ export declare const hashPassword: (password: string) => string; /** * 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; } export declare const memoryGrantStore: () => GrantStore; export declare const memoryProvider: (instance?: string) => StorageProvider; export declare const memoryRefStore: () => RefStore; /** What a multipart ticket authorizes: one direct-to-bucket multipart session. */ export declare interface MultipartTicketPayload extends Omit { /** The FINAL bucket key the parts assemble into. */ readonly key: string; /** The provider's multipart upload id. */ readonly uploadId: string; /** Total expected byte size. */ readonly size: number; } /** Result of a provider `put` / `head`. */ export declare interface ObjectMeta { readonly size: number; readonly etag?: string; } /** Parse a clamd INSTREAM reply: `stream: OK` → clean; `stream: FOUND` * → infected; anything else → throws. Exposed for testing. */ export declare const parseClamavResponse: (raw: string) => StorageScanResult; /** Parse transform params from a raw query string. Returns null when none * are present (→ caller serves the original / 302s to the CDN). */ export declare const parseTransformParams: (query: string) => TransformParams | null; export declare type PathDurability = 'separate-mount' | 'same-device-as-root' | 'unknown'; /** * Compare `path`'s filesystem against the root filesystem's. * * Never throws: a path that does not exist yet, or a platform that reports no * device, answers `unknown` — a diagnostic must not be the thing that fails. */ export declare const pathDurability: (path: string) => PathDurability; /** A ref with the server-only `passwordHash` removed — safe to serialize. */ export declare type PublicStorageRef = Omit; /** * Decode a data-URI avatar and store it under * `avatars//avatar.` for the given tenant as a PUBLIC object * (avatars are shown to anyone — served direct from the bucket/CDN). * Returns the ref; its id drives the `/_voltro/storage/:id` URL. */ export declare const putAvatar: (storage: StorageServiceShape, args: { readonly tenantId: string | null; readonly userId: string; readonly dataUri: string; }) => Effect.Effect; /** 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; } /** * Write a stream to an object, using the provider's `putStream` when it has one * and otherwise buffering into `put`. Mirror of {@link getObjectStream} for the * import path. */ export declare const putObjectStream: (provider: StorageProvider, key: string, stream: ByteStream, meta: { readonly contentType: string; readonly public?: boolean; readonly totalSize?: number; }) => Effect.Effect; /* Excluded from this release type: readStorageBuffer */ /** Does `ref` satisfy the resolved filter? Shared by both store impls so the * memory + DataStore paths match byte-for-byte (folder-PREFIX, all-tags AND, * owner, tenant, originals-only). Folder matches an exact value OR a `/`- * delimited descendant (`photos` ⇒ `photos`, `photos/2026`; NOT `photos-old`). */ export declare const refMatchesListQuery: (ref: StorageRef, q: ResolvedListQuery) => boolean; /** Where blob METADATA rows live (the bytes are in the provider). */ export declare interface RefStore { readonly insert: (ref: StorageRef) => Effect.Effect; readonly getById: (id: string) => Effect.Effect; /** Remove one ref row. Returns whether a row was ACTUALLY removed — the * caller gates the usage release on it so two concurrent deletes of the * same id refund the tenant counter exactly once. */ readonly delete: (id: string) => Effect.Effect; readonly listRecent: (limit: number) => Effect.Effect, StorageError>; /** How many refs (in this bucket) point at this provider key — drives * refcount-before-delete so a dedup'd object isn't yanked from under a * sibling ref. */ readonly countByKey: (bucket: string, key: string) => Effect.Effect; /** Total bytes + object count for a tenant — LIVE sum over the ref rows * (reporting / dashboards). The quota GATE does not read this — it rides * the atomic `consumeUsage` counter, which a read-then-compare here could * never make race-safe. */ readonly usageByTenant: (tenantId: string | null) => Effect.Effect<{ readonly bytes: number; readonly count: number; }, StorageError>; /** ATOMICALLY consume per-tenant usage headroom (the quota gate) — called * BEFORE the object write. When `quota` is given the consume only happens * if the post-consume totals stay within it (`allowed: false` otherwise, * nothing consumed); without `quota` it records the delta unconditionally, * so the counter stays authoritative even for apps that enable a quota * later. Returns the PRE-consume reading. Check-then-act is forbidden * here: memory does it in one synchronous tick, the DataStore impl runs a * compare-and-set loop over the UNIQUE `(tenantId)` counter row — correct * across replicas, not just one process. */ readonly consumeUsage: (tenantId: string | null, delta: UsageDelta, quota?: StorageQuota) => Effect.Effect<{ readonly allowed: boolean; readonly bytes: number; readonly count: number; }, StorageError>; /** Inverse of `consumeUsage` — refund a reservation whose object write * failed, or release a deleted ref's usage. Floors at zero. */ readonly releaseUsage: (tenantId: string | null, delta: UsageDelta) => Effect.Effect; /** Refs derived from a parent (transcode renditions / posters). */ readonly listByDerivedFrom: (parentId: string) => Effect.Effect, StorageError>; /** Browse the ref index filtered by folder-prefix / tags / ownerId / tenant, * newest-first, offset-paged. Backs `StorageService.listRefs`. Fetches * `limit + 1` so the service can report whether another page follows. */ readonly listRefs: (query: ResolvedListQuery) => Effect.Effect, StorageError>; } /** 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; } /** A `ListRefsInput` after the service normalizes its defaults / clamps — what * a `RefStore.listRefs` impl receives. `limit` is already the `+1` probe. */ export declare interface ResolvedListQuery { readonly tenantId: string | null | undefined; readonly folder: string | undefined; readonly tags: ReadonlyArray; readonly ownerId: string | undefined; readonly includeDerived: boolean; readonly limit: number; readonly offset: number; } export declare const resolveStorageProvider: (options: StoragePluginOptions) => StorageProvider; /** * The signing secret. A dedicated `VOLTRO_STORAGE_SECRET` is preferred so * grant tokens and session cookies can be rotated independently; absent * that we fall back to the session secret so it works out of the box. */ export declare const resolveStorageSecret: () => string; /** What a resumable ticket authorizes: one chunked upload session, until `exp`. */ export declare interface ResumableTicketPayload extends Omit { /** Upload-session id (random) — namespaces this upload's chunk objects. */ readonly id: string; /** Total expected byte size (validated at finalize; also the effective cap). */ readonly size: number; } export declare const s3Provider: (options: S3ProviderOptions) => StorageProvider; export declare interface S3ProviderOptions { readonly bucket: string; readonly region?: string; readonly endpoint?: string; readonly accessKeyId?: string; readonly secretAccessKey?: string; /** MinIO + most non-AWS S3 need path-style addressing. */ readonly forcePathStyle?: boolean; /** Display name — 'minio' for MinIO, else 's3'. */ readonly name?: string; /** Base URL for serving PUBLIC objects without the app (a CDN in front * of the bucket, or an R2/GCS public domain). `${cdnBaseUrl}/${key}`. */ readonly cdnBaseUrl?: string; /** Send `ACL: public-read` on public puts. Default true. Set false for * R2 / buckets that reject per-object ACLs (use a bucket policy + * `cdnBaseUrl` instead). */ readonly publicAcl?: boolean; } /** Register the app's storage provider (or clear with `undefined`). */ export declare const setActiveStorageProvider: (provider: StorageProvider | undefined) => void; /** Sign a finalize token valid for `ttlSeconds`. */ export declare const signFinalizeTicket: (fields: Omit, ttlSeconds: number, secret?: string) => string; /** Mint a token granting read access to `refId` for `ttlSeconds`. */ export declare const signGrant: (refId: string, ttlSeconds: number, secret?: string) => string; /** Sign a multipart ticket valid for `ttlSeconds`. */ export declare const signMultipartTicket: (fields: Omit, ttlSeconds: number, secret?: string) => string; /** Sign a resumable ticket valid for `ttlSeconds`. */ export declare const signResumableTicket: (fields: Omit, ttlSeconds: number, secret?: string) => string; /** Sign an upload ticket valid for `ttlSeconds`. */ export declare const signUploadTicket: (fields: Omit, ttlSeconds: number, secret?: string) => string; /** True if the bytes match the declared content type's magic number, OR we * have no signature for that type (can't verify → don't block). */ export declare const sniffMatches: (bytes: Uint8Array, contentType: string) => boolean; export declare const STORAGE_BLOBS_TABLE = "_voltro_storage_blobs"; export declare const STORAGE_GRANTS_TABLE = "_voltro_storage_grants"; export declare const STORAGE_REFS_TABLE = "_voltro_storage_refs"; export declare const STORAGE_USAGE_TABLE = "_voltro_storage_usage"; /** * 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; export declare const storagePlugin: (options?: StoragePluginOptions) => VoltroPlugin; export declare interface StoragePluginOptions { /** `'s3' | 'minio' | 'filesystem' | 'memory'` (built from env) or a * `StorageProvider`. Default: `STORAGE_PROVIDER` env, else `'memory'`. */ readonly provider?: StorageProviderName | StorageProvider; /** Bucket name (s3/minio). Default `S3_BUCKET`/`STORAGE_BUCKET` env, else 'voltro'. */ readonly bucket?: string; readonly region?: string; readonly endpoint?: string; readonly accessKeyId?: string; readonly secretAccessKey?: string; /** Filesystem provider root. Default `STORAGE_ROOT` env, else '.voltro-storage'. */ readonly root?: string; /** Azure storage account name (provider `'azure'`). Env `AZURE_STORAGE_ACCOUNT`. */ readonly accountName?: string; /** Azure account key — needed to mint SAS URLs. Env `AZURE_STORAGE_KEY`. */ readonly accountKey?: string; /** Azure connection string (alternative to account name/key). Env `AZURE_STORAGE_CONNECTION_STRING`. */ readonly connectionString?: string; /** Public-object base URL (a CDN in front of the bucket, or an R2/GCS * public domain). `${cdnBaseUrl}/${key}`. */ readonly cdnBaseUrl?: string; /** Absolute public base URL of THIS api (`scheme://host[:port]`). Env * `VOLTRO_PUBLIC_URL`. Prepended to the `/_voltro/storage/:id` serve URL so * `getUrl`/`mintUrl`/the upload route return ABSOLUTE URLs — required when * the api is a different origin than the app (browser `` + * server-to-server AI fetches). Omit for same-origin (relative URL). */ readonly publicBaseUrl?: string; /** Origins allowed to read the serve route + POST the upload route * cross-origin (CORS). Env `STORAGE_ALLOWED_ORIGINS` (comma-separated), or * `'*'` for any. Omit for same-origin only. */ readonly allowedOrigins?: ReadonlyArray | '*'; /** Normalize raster images on upload: apply EXIF orientation + strip ALL * metadata (GPS/camera), re-encoding the bytes. Off by default. Width/height * + LQIP placeholder are extracted regardless. */ readonly normalizeImages?: boolean; /** Per-tenant quota enforced on upload (object count / total bytes). The * headroom is consumed atomically (a CAS over the `_voltro_storage_usage` * counter row) BEFORE the bytes are stored, so concurrent uploads across * replicas cannot overshoot the cap. Exceeding it fails with * `StorageRejected` (`'quota-exceeded'`). */ readonly quota?: { readonly maxBytes?: number; readonly maxCount?: number; }; /** Send `ACL: public-read` on public puts (s3). Default true; set false * for R2 / buckets that reject per-object ACLs. */ readonly publicAcl?: boolean; /** Metadata store: a `RefStore` or `'memory'` (default). A DB-backed * store is wired by the serve pipeline once the app's DataStore exists. */ readonly refStore?: 'memory' | RefStore; /** Grant store: a `GrantStore` or `'memory'` (default). DB-backed once * the DataStore is available, like `refStore`. */ readonly grantStore?: 'memory' | GrantStore; /** Static key prefix prepended to every object (app/env namespace). */ readonly tenantPrefix?: string; /** Transient-failure retries. Default 3. */ readonly attempts?: number; /** Default presigned-URL / grant-token TTL (seconds). Default 3600. */ readonly urlExpiresInSec?: number; /** Access engine config — a group resolver + named custom guards. */ readonly access?: AccessOptions; /** Upload constraints — size cap, content-type allow-list, magic-byte sniff. */ readonly limits?: UploadLimits; /** Virus scanner run on every `put` before bytes are stored (e.g. * `clamavScanner({...})`). A detection fails with `StorageScanRejected`. */ readonly scan?: StorageScanner; /** App-supplied probe for video/audio uploads — extracts duration + a poster * frame (stored as the ref's `placeholder`). The core bundles no transcoder; * wire ffprobe / a cloud API. Best-effort — never blocks an upload. */ readonly videoProbe?: VideoProbe; /** Transcode step for video/audio — produces renditions (720p, webm, poster, * …) stored as linked refs. Use the first-party `ffmpegTranscoder({...})` or * bring your own. Runs in the background after upload. */ readonly transcode?: Transcoder; /** Auto-run `transcode` after a video/audio upload. Default ON when a * transcoder is set; false to only transcode via `service.transcode(refId)`. */ readonly autoTranscode?: boolean; /** Disambiguates multiple instances of this plugin in one app. */ /** * Namespace for this plugin's rpc tags + inspect endpoints. Default `storage`. * * Set it when your app already publishes under that name — an exact tag * collision is fatal at codegen, and this is the way out. Orthogonal to * `name` below: `alias` REPLACES the namespace, `name` distinguishes two * installations within it. * * The cost, stated because nothing else states it: the local and cloud * dashboards fetch this plugin's panel at the DEFAULT slug, so an aliased * install keeps working while its dashboard panel 404s. Alias to escape a * collision, not for taste. */ readonly alias?: string; /** * Discriminator for a SECOND installation of this plugin, when one app runs * two (`@voltro/plugin-storage#analytics`). Not a rename — for that use `alias`. */ readonly name?: string; } /** * 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'; /** Per-tenant upload budget (see `StorageServiceOptions.quota`). */ export declare interface StorageQuota { readonly maxBytes?: number; readonly maxCount?: number; } /** * 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; export declare const storageServiceLayer: (options: StorageServiceOptions) => Layer.Layer; export declare interface StorageServiceOptions { readonly provider: StorageProvider; readonly refStore: RefStore; readonly grantStore: GrantStore; readonly bucket: string; /** Static prefix prepended to every key (e.g. an app/env namespace). */ readonly tenantPrefix?: string; /** Transient-failure retries (429 / 5xx / network). Default 3. */ readonly attempts?: number; /** Default presigned-URL / grant-token TTL (seconds). Default 3600. */ readonly urlExpiresInSec?: number; /** Group resolver + custom guards for the access engine. */ readonly access?: AccessOptions; /** Upload constraints — size cap, content-type allow-list, magic-byte sniff. */ readonly limits?: UploadLimits; /** Virus scanner run on `put` before the bytes are stored. */ readonly scan?: StorageScanner; /** App-supplied probe for video/audio uploads (duration + poster frame). * The core bundles no transcoder; wire ffprobe / a cloud API. Best-effort. */ readonly videoProbe?: VideoProbe; /** App-supplied (or first-party `ffmpegTranscoder`) transcode step producing * renditions from video/audio uploads. */ readonly transcode?: Transcoder; /** Auto-run `transcode` in the background after a video/audio upload. Defaults * to ON when a transcoder is configured; set false to only transcode when the * app explicitly calls `service.transcode(refId)` (e.g. from a durable job). */ readonly autoTranscode?: boolean; /** Absolute public base URL of THIS api (`scheme://host[:port]`) — prepended * to the `/_voltro/storage/:id` serve URL so `getUrl`/`mintUrl`/the upload * route emit ABSOLUTE URLs. Required when the api is a different origin than * the app: a browser `` and server-to-server AI-gateway fetches both * need an absolute, reachable URL. Omit for same-origin (relative URL). */ readonly publicBaseUrl?: string; /** Normalize raster images on upload: apply EXIF orientation, then strip ALL * metadata (GPS/camera) and re-encode. Off by default (it re-encodes, which * changes the bytes + content-address). Width/height/placeholder are always * extracted regardless. */ readonly normalizeImages?: boolean; /** Per-tenant quota enforced on `put` (and multipart completion). The * headroom is consumed ATOMICALLY (`RefStore.consumeUsage`) before the * bytes are stored — concurrent uploads across replicas cannot overshoot * the cap — and refunded if the write fails. Exceeding it fails with * `StorageRejected` (reason `'quota-exceeded'`) before any bytes land. */ readonly quota?: StorageQuota; } /** 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; } /** * A ByteStream → Node `Readable`, for SDK uploaders that consume Node streams * (s3 `Upload`, azure `uploadStream`) or `stream.pipeline` to a file. Bridges * via a Web ReadableStream, which Effect produces natively. */ export declare const toNodeReadable: (stream: ByteStream) => Promise; /** * 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>; /** Transform with a small FIFO cache keyed by `(refId, params)` so repeated * requests for the same variant don't re-encode. */ export declare const transformCached: (refId: string, bytes: Uint8Array, contentType: string, params: TransformParams) => Promise; export declare interface Transformed { readonly bytes: Uint8Array; readonly contentType: string; } /** * Transform image bytes per `params`. Returns null (→ serve original) when * the content isn't a raster image, `sharp` isn't installed, or processing * fails (graceful — a bad transform should never 500 a valid object). */ export declare const transformImage: (bytes: Uint8Array, contentType: string, params: TransformParams) => Promise; export declare interface TransformParams { readonly w?: number; readonly h?: number; readonly format?: string; readonly q?: number; readonly fit?: string; } 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; } /** What an upload ticket authorizes: a single through-app upload bound to an * owner/tenant/content-type/visibility, until `exp`. */ export declare interface UploadTicketPayload { /** ownerId (subject id) the stored object is attributed to. */ readonly sub: string | null; /** tenantId that scopes the key prefix + ref row. */ readonly tenant: string | null; /** Authorized content-type — the stored object uses this. */ readonly ct: string; readonly vis: 'public' | 'private'; /** Optional fixed logical key (else content-addressed). */ readonly key?: string; /** Max bytes the route will accept for this ticket (0/undefined = no cap). */ readonly max?: number; /** App metadata carried onto the stored ref (so apps need no parallel table). */ readonly folder?: string; readonly tags?: ReadonlyArray; readonly alt?: string; readonly caption?: string; /** Unix seconds when the ticket expires. */ readonly exp: number; } /** One reservation's worth of usage — the object's stored size + one ref. */ export declare interface UsageDelta { readonly bytes: number; readonly count: number; } /** Verify a finalize token. Returns the payload on success, null on any failure * (malformed, bad signature, expired, missing temp key). Never throws. */ export declare const verifyFinalizeTicket: (token: string, secret?: string) => FinalizeTicketPayload | null; /** * Verify a grant token. Returns the payload on success, null on any * failure (malformed, bad signature, expired). Never throws. */ export declare const verifyGrant: (token: string, secret?: string) => GrantTokenPayload | null; /** Verify a multipart ticket. Returns the payload on success, null on any * failure (malformed, bad signature, expired, missing key/uploadId). */ export declare const verifyMultipartTicket: (token: string, secret?: string) => MultipartTicketPayload | null; /** Constant-time check of a plaintext password against a stored hash. */ export declare const verifyPassword: (password: string | undefined, stored: string | null) => boolean; /** Verify a resumable ticket. Returns the payload on success, null on any * failure (malformed, bad signature, expired, missing id/size). Never throws. */ export declare const verifyResumableTicket: (token: string, secret?: string) => ResumableTicketPayload | null; /** Verify an upload ticket. Returns the payload on success, null on any * failure (malformed, bad signature, expired). Never throws. */ export declare const verifyUploadTicket: (token: string, secret?: string) => UploadTicketPayload | null; /** * 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 `