@voltro/plugin-storage 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1255 @@
1
+ import { ColumnBuilder } from '@voltro/database';
2
+ import { ColumnType } from '@voltro/database';
3
+ import { Context } from 'effect';
4
+ import { DataStore } from '@voltro/database';
5
+ import { Effect } from 'effect';
6
+ import { Layer } from 'effect';
7
+ import { mixin } from '@voltro/database';
8
+ import { Readable } from 'node:stream';
9
+ import { Schema } from 'effect';
10
+ import { Stream } from 'effect';
11
+ import { Subject } from '@voltro/protocol';
12
+ import { TableLike } from '@voltro/database';
13
+ import { VoltroPlugin } from '@voltro/protocol';
14
+
15
+ export declare interface AccessOptions {
16
+ /** Resolve a subject's group ids. Default: `subject.metadata.groups`. */
17
+ readonly resolveGroups?: (subject: Subject) => ReadonlyArray<string> | Promise<ReadonlyArray<string>>;
18
+ /** Named custom guards referenced by `rule.guard`. */
19
+ readonly guards?: Record<string, StorageGuard>;
20
+ }
21
+
22
+ export declare type AccessPolicy = ReadonlyArray<AccessRule>;
23
+
24
+ /**
25
+ * One access rule. A rule GRANTS when EVERY present condition is satisfied
26
+ * (AND). A policy is an array of rules; access is allowed when ANY rule
27
+ * grants (OR). An empty policy on a private object means owner-only (the
28
+ * implicit owner check always applies).
29
+ */
30
+ export declare interface AccessRule {
31
+ /** `subject.id === ref.ownerId`. */
32
+ readonly owner?: boolean;
33
+ /** `subject.metadata.roles` intersects these. */
34
+ readonly roles?: ReadonlyArray<string>;
35
+ /** resolved groups (default `subject.metadata.groups`) intersect these. */
36
+ readonly groups?: ReadonlyArray<string>;
37
+ /** `subject.scopes` intersects these. */
38
+ readonly scopes?: ReadonlyArray<string>;
39
+ /** `subject.tenantId === ref.tenantId`. */
40
+ readonly tenant?: boolean;
41
+ /** subject is any authenticated api-key. */
42
+ readonly apiKey?: boolean;
43
+ /** a correct password was supplied (checked at mint, against passwordHash). */
44
+ readonly password?: boolean;
45
+ /** a named custom guard (`options.guards[name]`) returns true. */
46
+ readonly guard?: string;
47
+ }
48
+
49
+ /** A column binding an entity to a stored asset (a `_voltro_storage_refs` id). */
50
+ export declare function assetRef(options: AssetRefOptions & {
51
+ readonly nullable: false;
52
+ }): ColumnBuilder<string, ColumnType>;
53
+
54
+ export declare function assetRef(options?: AssetRefOptions): ColumnBuilder<string | null, ColumnType>;
55
+
56
+ export declare interface AssetRefOptions {
57
+ /** Enforce a DB foreign key to `_voltro_storage_refs.id`. Default true. */
58
+ readonly fk?: boolean;
59
+ /** FK onDelete when `fk` is true. Default `'setNull'`. */
60
+ readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
61
+ /** Nullable? Default true — an entity may have no asset yet. */
62
+ readonly nullable?: boolean;
63
+ }
64
+
65
+ export declare const azureProvider: (options: AzureProviderOptions) => StorageProvider;
66
+
67
+ export declare interface AzureProviderOptions {
68
+ /** Container name (the "bucket"). */
69
+ readonly container: string;
70
+ /** Storage account name (for the default `https://<account>.blob.core.windows.net` host + SAS). */
71
+ readonly accountName?: string;
72
+ /** Account key — required to mint SAS URLs (presigned get/put). */
73
+ readonly accountKey?: string;
74
+ /** Full connection string (alternative to accountName/accountKey for the client; SAS still needs the key). */
75
+ readonly connectionString?: string;
76
+ /** Custom blob endpoint (e.g. Azurite `http://127.0.0.1:10000/devstoreaccount1`). */
77
+ readonly endpoint?: string;
78
+ /** Public base URL for public objects (a CDN / public container host). */
79
+ readonly cdnBaseUrl?: string;
80
+ }
81
+
82
+ export declare const buildStorageService: (options: StorageServiceOptions) => StorageServiceShape;
83
+
84
+ /** Slice a buffer into a lazy chunked ByteStream. Backs the buffered fallback
85
+ * + the in-memory/DB providers that have a whole buffer, not a backend
86
+ * stream. `subarray` keeps the chunks as views (no copy). */
87
+ export declare const bytesToStream: (bytes: Uint8Array, chunkSize?: number) => ByteStream;
88
+
89
+ /**
90
+ * A lazy stream of bytes — the streaming counterpart to {@link StoredObject}'s
91
+ * eager `bytes`. Backpressured: chunks are pulled on demand, so a multi-GB blob
92
+ * never fully materialises in memory. The error channel is {@link StorageError}
93
+ * so a stream handed to `putStream` carries the same typed failure the rest of
94
+ * the transport does (callers map their source errors into it).
95
+ */
96
+ export declare type ByteStream = Stream.Stream<Uint8Array, StorageError>;
97
+
98
+ /** Check an upload against the limits. Returns `null` when ok, else a
99
+ * `{ reason, detail }` describing the violation. */
100
+ export declare const checkLimits: (bytes: Uint8Array, contentType: string, limits: UploadLimits | undefined) => {
101
+ readonly reason: string;
102
+ readonly detail: string;
103
+ } | null;
104
+
105
+ export declare interface ClamavOptions {
106
+ /** clamd host. Default `127.0.0.1` (env `CLAMAV_HOST`). */
107
+ readonly host?: string;
108
+ /** clamd port. Default `3310` (env `CLAMAV_PORT`). */
109
+ readonly port?: number;
110
+ /** Abort + reject after this many ms. Default 30_000. */
111
+ readonly timeoutMs?: number;
112
+ /** INSTREAM chunk size. Default 64 KiB. */
113
+ readonly chunkSize?: number;
114
+ }
115
+
116
+ /**
117
+ * A `StorageScanner` backed by a running clamd. Wire it via
118
+ * `storagePlugin({ scan: clamavScanner({ host, port }) })`. A scanner error
119
+ * (clamd unreachable) surfaces as a transient `StorageError` from `put` —
120
+ * fail-closed, so a misconfigured scanner doesn't silently wave files
121
+ * through. An actual detection fails with `StorageScanRejected`.
122
+ */
123
+ export declare const clamavScanner: (options?: ClamavOptions) => StorageScanner;
124
+
125
+ /* Excluded from this release type: clearStorageBuffer */
126
+
127
+ /** Drain a ByteStream into a single Uint8Array (buffered fallback / small
128
+ * blobs). Defeats the memory win — only for backends that can't stream. */
129
+ export declare const collectStream: (stream: ByteStream) => Effect.Effect<Uint8Array, StorageError>;
130
+
131
+ /** Concatenate byte chunks into one contiguous Uint8Array. */
132
+ export declare const concatBytes: (chunks: ReadonlyArray<Uint8Array>) => Uint8Array;
133
+
134
+ /** `image/*` matches `image/png`; exact otherwise. */
135
+ export declare const contentTypeAllowed: (contentType: string, allowed: ReadonlyArray<string> | undefined) => boolean;
136
+
137
+ /**
138
+ * Stores blob bytes IN the app's database (`_voltro_storage_blobs`) via the
139
+ * bound DataStore — no external object store. The plugin wires the DataStore
140
+ * through `bindDataStore` once it exists (the provider can't be used before
141
+ * that). For small files; large blobs belong in object storage.
142
+ */
143
+ export declare const databaseProvider: () => StorageProvider & {
144
+ bindDataStore: (store: unknown) => void;
145
+ };
146
+
147
+ export declare const dataStoreGrantStore: (store: DataStore) => GrantStore;
148
+
149
+ /** Ref store backed by the app's `DataStore` (`_voltro_storage_refs`). */
150
+ export declare const dataStoreRefStore: (store: DataStore) => RefStore;
151
+
152
+ /** Decode a `data:<mime>;base64,<data>` URI to bytes + content type. */
153
+ export declare const decodeDataUri: (dataUri: string) => {
154
+ bytes: Uint8Array;
155
+ contentType: string;
156
+ };
157
+
158
+ /** Frame bytes for clamd `INSTREAM`: `zINSTREAM\0` then `<u32 len><data>`
159
+ * chunks, terminated by a zero-length chunk. Exposed for testing. */
160
+ export declare const encodeInstream: (bytes: Uint8Array, chunkSize?: number) => Buffer;
161
+
162
+ export declare interface FfmpegRenditionSpec {
163
+ /** Derivative class stored on the rendition ref — `'rendition'` / `'poster'`. */
164
+ readonly kind: string;
165
+ /** Human label stored as the rendition's caption — `'720p'`, `'webm'`. */
166
+ readonly label?: string;
167
+ /** Output content type — `'video/mp4'`, `'video/webm'`, `'image/jpeg'`. */
168
+ readonly contentType: string;
169
+ /** Output file extension, no dot — `'mp4'`, `'webm'`, `'jpg'`. */
170
+ readonly ext: string;
171
+ /** ffmpeg OUTPUT args (between `-i <in>` and the output file). e.g.
172
+ * `['-vf','scale=-2:720','-c:v','libx264','-crf','23','-c:a','aac']`. */
173
+ readonly args: ReadonlyArray<string>;
174
+ }
175
+
176
+ /** A {@link Transcoder} backed by the ffmpeg binary — see the file header. */
177
+ export declare const ffmpegTranscoder: (opts: FfmpegTranscoderOptions) => Transcoder;
178
+
179
+ export declare interface FfmpegTranscoderOptions {
180
+ readonly renditions: ReadonlyArray<FfmpegRenditionSpec>;
181
+ /** Path to the ffmpeg binary. Default `'ffmpeg'` (resolved on PATH). */
182
+ readonly ffmpegPath?: string;
183
+ /** Per-rendition timeout in ms. Default 5 minutes. */
184
+ readonly timeoutMs?: number;
185
+ }
186
+
187
+ export declare const filesystemProvider: (options: FilesystemProviderOptions) => StorageProvider;
188
+
189
+ export declare interface FilesystemProviderOptions {
190
+ readonly root: string;
191
+ }
192
+
193
+ /** What a finalize token authorizes: promoting ONE temp-key blob into a real
194
+ * ref under the bound identity, until `exp`. */
195
+ export declare interface FinalizeTicketPayload extends Omit<UploadTicketPayload, 'key'> {
196
+ /** The TEMP bucket key the client PUT its bytes to (required — finalize reads
197
+ * exactly this key, so it can't be aimed at another object). */
198
+ readonly key: string;
199
+ }
200
+
201
+ /**
202
+ * A Node `Readable` (yields Buffer/Uint8Array) → ByteStream. `make` is called
203
+ * lazily on first pull so the fd / HTTP body isn't opened until consumption
204
+ * starts. Read failures surface as a transient {@link StorageError}.
205
+ */
206
+ export declare const fromNodeReadable: (provider: string, make: () => Readable) => ByteStream;
207
+
208
+ /**
209
+ * Read an object as a stream, using the provider's `getStream` when it has one
210
+ * and otherwise buffering via `get`. The export layer calls this; it never has
211
+ * to know whether a given backend streams.
212
+ */
213
+ export declare const getObjectStream: (provider: StorageProvider, key: string) => Effect.Effect<StoredStream, StorageError>;
214
+
215
+ /** Input to `StorageService.grant`. */
216
+ export declare interface GrantInput {
217
+ readonly refId: string;
218
+ readonly principalType: GrantPrincipalType;
219
+ readonly principalId: string;
220
+ readonly permission?: GrantPermission;
221
+ readonly expiresAt?: Date | null;
222
+ readonly createdBy?: string | null;
223
+ }
224
+
225
+ export declare type GrantPermission = 'read' | 'write';
226
+
227
+ export declare type GrantPrincipalType = 'user' | 'group' | 'apiKey';
228
+
229
+ export declare interface GrantStore {
230
+ readonly insert: (grant: StorageGrant) => Effect.Effect<void, StorageError>;
231
+ readonly delete: (id: string) => Effect.Effect<void, StorageError>;
232
+ readonly listByRef: (refId: string) => Effect.Effect<ReadonlyArray<StorageGrant>, StorageError>;
233
+ readonly deleteByRef: (refId: string) => Effect.Effect<void, StorageError>;
234
+ }
235
+
236
+ /** What a grant token authorizes: read access to one ref until `exp`. */
237
+ export declare interface GrantTokenPayload {
238
+ readonly refId: string;
239
+ /** Unix seconds when the token expires. */
240
+ readonly exp: number;
241
+ }
242
+
243
+ /** Hash a file password as `salt:scryptHash` (hex). */
244
+ export declare const hashPassword: (password: string) => string;
245
+
246
+ /**
247
+ * Filter + page a `StorageService.listRefs` / media-library query. Every
248
+ * filter is optional and ANDed; omit them all to page the whole (tenant-scoped)
249
+ * ref set newest-first.
250
+ */
251
+ export declare interface ListRefsInput {
252
+ /** Scope to one tenant (`null` = the system/global partition). Omit to span
253
+ * every tenant — reserve that for trusted admin reads, not an end-user
254
+ * media library. */
255
+ readonly tenantId?: string | null;
256
+ /** Match refs whose `folder` equals this value OR sits UNDER it as a path
257
+ * prefix (`photos` matches `photos` and `photos/2026`). A trailing slash is
258
+ * normalized away. */
259
+ readonly folder?: string;
260
+ /** Match refs whose `tags` contain ALL of these (AND). */
261
+ readonly tags?: ReadonlyArray<string>;
262
+ /** Match refs created by this subject (the `ownerId` column). */
263
+ readonly ownerId?: string;
264
+ /** Only originals (`false`, the default — excludes transcode renditions /
265
+ * posters) or include derivatives too (`true`). */
266
+ readonly includeDerived?: boolean;
267
+ /** Page size. Clamped to `[1, 500]`; default 50. */
268
+ readonly limit?: number;
269
+ /** Rows to skip (offset paging). Default 0. */
270
+ readonly offset?: number;
271
+ }
272
+
273
+ /** One page of a `listRefs` query. `nextOffset` is non-null when another page
274
+ * exists (i.e. the store returned a full `limit + 1` probe) — pass it back as
275
+ * the next `offset`. */
276
+ export declare interface ListRefsResult {
277
+ readonly refs: ReadonlyArray<StorageRef>;
278
+ readonly nextOffset: number | null;
279
+ }
280
+
281
+ export declare const memoryGrantStore: () => GrantStore;
282
+
283
+ export declare const memoryProvider: (instance?: string) => StorageProvider;
284
+
285
+ export declare const memoryRefStore: () => RefStore;
286
+
287
+ /** What a multipart ticket authorizes: one direct-to-bucket multipart session. */
288
+ export declare interface MultipartTicketPayload extends Omit<UploadTicketPayload, 'key'> {
289
+ /** The FINAL bucket key the parts assemble into. */
290
+ readonly key: string;
291
+ /** The provider's multipart upload id. */
292
+ readonly uploadId: string;
293
+ /** Total expected byte size. */
294
+ readonly size: number;
295
+ }
296
+
297
+ /** Result of a provider `put` / `head`. */
298
+ export declare interface ObjectMeta {
299
+ readonly size: number;
300
+ readonly etag?: string;
301
+ }
302
+
303
+ /** Parse a clamd INSTREAM reply: `stream: OK` → clean; `stream: <T> FOUND`
304
+ * → infected; anything else → throws. Exposed for testing. */
305
+ export declare const parseClamavResponse: (raw: string) => StorageScanResult;
306
+
307
+ /** Parse transform params from a raw query string. Returns null when none
308
+ * are present (→ caller serves the original / 302s to the CDN). */
309
+ export declare const parseTransformParams: (query: string) => TransformParams | null;
310
+
311
+ /** A ref with the server-only `passwordHash` removed — safe to serialize. */
312
+ export declare type PublicStorageRef = Omit<StorageRef, 'passwordHash'>;
313
+
314
+ /**
315
+ * Decode a data-URI avatar and store it under
316
+ * `avatars/<userId>/avatar.<ext>` for the given tenant as a PUBLIC object
317
+ * (avatars are shown to anyone — served direct from the bucket/CDN).
318
+ * Returns the ref; its id drives the `/_voltro/storage/:id` URL.
319
+ */
320
+ export declare const putAvatar: (storage: StorageServiceShape, args: {
321
+ readonly tenantId: string | null;
322
+ readonly userId: string;
323
+ readonly dataUri: string;
324
+ }) => Effect.Effect<StorageRef, StorageError | StorageRejected | StorageScanRejected>;
325
+
326
+ /** Input to `StorageService.put`. */
327
+ export declare interface PutInput {
328
+ readonly bytes: Uint8Array;
329
+ readonly contentType: string;
330
+ /** Active org id — scopes the key prefix + the ref row. */
331
+ readonly tenantId?: string | null;
332
+ /** Subject id that owns the object (enables the `owner` access rule). */
333
+ readonly ownerId?: string | null;
334
+ /** Optional logical key suffix (e.g. `avatars/<userId>/x.png`); else a
335
+ * content-addressed key is derived from the checksum. */
336
+ readonly key?: string;
337
+ /** `private` (default) or `public`. */
338
+ readonly visibility?: StorageVisibility;
339
+ /** Access rules for a private object (in addition to the owner check). */
340
+ readonly access?: AccessPolicy;
341
+ /** Plaintext password — hashed by the service, never stored as-is. */
342
+ readonly password?: string;
343
+ readonly folder?: string;
344
+ readonly tags?: ReadonlyArray<string>;
345
+ readonly alt?: string;
346
+ readonly caption?: string;
347
+ /** Media duration in seconds (caller-supplied for video/audio). */
348
+ readonly duration?: number;
349
+ /** Parent ref id — set when storing a derived rendition/poster (internal). */
350
+ readonly derivedFrom?: string;
351
+ /** Derivative label — `'rendition'` / `'poster'` (internal). */
352
+ readonly kind?: string;
353
+ }
354
+
355
+ /**
356
+ * Write a stream to an object, using the provider's `putStream` when it has one
357
+ * and otherwise buffering into `put`. Mirror of {@link getObjectStream} for the
358
+ * import path.
359
+ */
360
+ export declare const putObjectStream: (provider: StorageProvider, key: string, stream: ByteStream, meta: {
361
+ readonly contentType: string;
362
+ readonly public?: boolean;
363
+ readonly totalSize?: number;
364
+ }) => Effect.Effect<ObjectMeta, StorageError>;
365
+
366
+ /* Excluded from this release type: readStorageBuffer */
367
+
368
+ /** Does `ref` satisfy the resolved filter? Shared by both store impls so the
369
+ * memory + DataStore paths match byte-for-byte (folder-PREFIX, all-tags AND,
370
+ * owner, tenant, originals-only). Folder matches an exact value OR a `/`-
371
+ * delimited descendant (`photos` ⇒ `photos`, `photos/2026`; NOT `photos-old`). */
372
+ export declare const refMatchesListQuery: (ref: StorageRef, q: ResolvedListQuery) => boolean;
373
+
374
+ /** Where blob METADATA rows live (the bytes are in the provider). */
375
+ export declare interface RefStore {
376
+ readonly insert: (ref: StorageRef) => Effect.Effect<void, StorageError>;
377
+ readonly getById: (id: string) => Effect.Effect<StorageRef | null, StorageError>;
378
+ /** Remove one ref row. Returns whether a row was ACTUALLY removed — the
379
+ * caller gates the usage release on it so two concurrent deletes of the
380
+ * same id refund the tenant counter exactly once. */
381
+ readonly delete: (id: string) => Effect.Effect<boolean, StorageError>;
382
+ readonly listRecent: (limit: number) => Effect.Effect<ReadonlyArray<StorageRef>, StorageError>;
383
+ /** How many refs (in this bucket) point at this provider key — drives
384
+ * refcount-before-delete so a dedup'd object isn't yanked from under a
385
+ * sibling ref. */
386
+ readonly countByKey: (bucket: string, key: string) => Effect.Effect<number, StorageError>;
387
+ /** Total bytes + object count for a tenant — LIVE sum over the ref rows
388
+ * (reporting / dashboards). The quota GATE does not read this — it rides
389
+ * the atomic `consumeUsage` counter, which a read-then-compare here could
390
+ * never make race-safe. */
391
+ readonly usageByTenant: (tenantId: string | null) => Effect.Effect<{
392
+ readonly bytes: number;
393
+ readonly count: number;
394
+ }, StorageError>;
395
+ /** ATOMICALLY consume per-tenant usage headroom (the quota gate) — called
396
+ * BEFORE the object write. When `quota` is given the consume only happens
397
+ * if the post-consume totals stay within it (`allowed: false` otherwise,
398
+ * nothing consumed); without `quota` it records the delta unconditionally,
399
+ * so the counter stays authoritative even for apps that enable a quota
400
+ * later. Returns the PRE-consume reading. Check-then-act is forbidden
401
+ * here: memory does it in one synchronous tick, the DataStore impl runs a
402
+ * compare-and-set loop over the UNIQUE `(tenantId)` counter row — correct
403
+ * across replicas, not just one process. */
404
+ readonly consumeUsage: (tenantId: string | null, delta: UsageDelta, quota?: StorageQuota) => Effect.Effect<{
405
+ readonly allowed: boolean;
406
+ readonly bytes: number;
407
+ readonly count: number;
408
+ }, StorageError>;
409
+ /** Inverse of `consumeUsage` — refund a reservation whose object write
410
+ * failed, or release a deleted ref's usage. Floors at zero. */
411
+ readonly releaseUsage: (tenantId: string | null, delta: UsageDelta) => Effect.Effect<void, StorageError>;
412
+ /** Refs derived from a parent (transcode renditions / posters). */
413
+ readonly listByDerivedFrom: (parentId: string) => Effect.Effect<ReadonlyArray<StorageRef>, StorageError>;
414
+ /** Browse the ref index filtered by folder-prefix / tags / ownerId / tenant,
415
+ * newest-first, offset-paged. Backs `StorageService.listRefs`. Fetches
416
+ * `limit + 1` so the service can report whether another page follows. */
417
+ readonly listRefs: (query: ResolvedListQuery) => Effect.Effect<ReadonlyArray<StorageRef>, StorageError>;
418
+ }
419
+
420
+ /** One derived output a `Transcoder` produces from a source asset. Each becomes
421
+ * a normal storage ref linked back to the original via `derivedFrom`. */
422
+ export declare interface Rendition {
423
+ readonly bytes: Uint8Array;
424
+ readonly contentType: string;
425
+ /** Derivative class — e.g. `'rendition'` (a playable variant) or `'poster'`. */
426
+ readonly kind: string;
427
+ /** Human label stored as the ref's `caption` — e.g. `'720p'`, `'webm'`. */
428
+ readonly label?: string;
429
+ }
430
+
431
+ /** A `ListRefsInput` after the service normalizes its defaults / clamps — what
432
+ * a `RefStore.listRefs` impl receives. `limit` is already the `+1` probe. */
433
+ export declare interface ResolvedListQuery {
434
+ readonly tenantId: string | null | undefined;
435
+ readonly folder: string | undefined;
436
+ readonly tags: ReadonlyArray<string>;
437
+ readonly ownerId: string | undefined;
438
+ readonly includeDerived: boolean;
439
+ readonly limit: number;
440
+ readonly offset: number;
441
+ }
442
+
443
+ export declare const resolveStorageProvider: (options: StoragePluginOptions) => StorageProvider;
444
+
445
+ /**
446
+ * The signing secret. A dedicated `VOLTRO_STORAGE_SECRET` is preferred so
447
+ * grant tokens and session cookies can be rotated independently; absent
448
+ * that we fall back to the session secret so it works out of the box.
449
+ */
450
+ export declare const resolveStorageSecret: () => string;
451
+
452
+ /** What a resumable ticket authorizes: one chunked upload session, until `exp`. */
453
+ export declare interface ResumableTicketPayload extends Omit<UploadTicketPayload, 'key'> {
454
+ /** Upload-session id (random) — namespaces this upload's chunk objects. */
455
+ readonly id: string;
456
+ /** Total expected byte size (validated at finalize; also the effective cap). */
457
+ readonly size: number;
458
+ }
459
+
460
+ export declare const s3Provider: (options: S3ProviderOptions) => StorageProvider;
461
+
462
+ export declare interface S3ProviderOptions {
463
+ readonly bucket: string;
464
+ readonly region?: string;
465
+ readonly endpoint?: string;
466
+ readonly accessKeyId?: string;
467
+ readonly secretAccessKey?: string;
468
+ /** MinIO + most non-AWS S3 need path-style addressing. */
469
+ readonly forcePathStyle?: boolean;
470
+ /** Display name — 'minio' for MinIO, else 's3'. */
471
+ readonly name?: string;
472
+ /** Base URL for serving PUBLIC objects without the app (a CDN in front
473
+ * of the bucket, or an R2/GCS public domain). `${cdnBaseUrl}/${key}`. */
474
+ readonly cdnBaseUrl?: string;
475
+ /** Send `ACL: public-read` on public puts. Default true. Set false for
476
+ * R2 / buckets that reject per-object ACLs (use a bucket policy +
477
+ * `cdnBaseUrl` instead). */
478
+ readonly publicAcl?: boolean;
479
+ }
480
+
481
+ /** Sign a finalize token valid for `ttlSeconds`. */
482
+ export declare const signFinalizeTicket: (fields: Omit<FinalizeTicketPayload, "exp">, ttlSeconds: number, secret?: string) => string;
483
+
484
+ /** Mint a token granting read access to `refId` for `ttlSeconds`. */
485
+ export declare const signGrant: (refId: string, ttlSeconds: number, secret?: string) => string;
486
+
487
+ /** Sign a multipart ticket valid for `ttlSeconds`. */
488
+ export declare const signMultipartTicket: (fields: Omit<MultipartTicketPayload, "exp">, ttlSeconds: number, secret?: string) => string;
489
+
490
+ /** Sign a resumable ticket valid for `ttlSeconds`. */
491
+ export declare const signResumableTicket: (fields: Omit<ResumableTicketPayload, "exp">, ttlSeconds: number, secret?: string) => string;
492
+
493
+ /** Sign an upload ticket valid for `ttlSeconds`. */
494
+ export declare const signUploadTicket: (fields: Omit<UploadTicketPayload, "exp">, ttlSeconds: number, secret?: string) => string;
495
+
496
+ /** True if the bytes match the declared content type's magic number, OR we
497
+ * have no signature for that type (can't verify → don't block). */
498
+ export declare const sniffMatches: (bytes: Uint8Array, contentType: string) => boolean;
499
+
500
+ export declare const STORAGE_BLOBS_TABLE = "_voltro_storage_blobs";
501
+
502
+ export declare const STORAGE_GRANTS_TABLE = "_voltro_storage_grants";
503
+
504
+ export declare const STORAGE_REFS_TABLE = "_voltro_storage_refs";
505
+
506
+ export declare const STORAGE_USAGE_TABLE = "_voltro_storage_usage";
507
+
508
+ /**
509
+ * Access to a private object was refused. Distinct from `StorageError` so
510
+ * handlers can tell "not allowed" (403) from "backend blew up" (5xx) and
511
+ * the wire-error union carries it typed.
512
+ */
513
+ export declare class StorageAccessDenied extends StorageAccessDenied_base {
514
+ }
515
+
516
+ declare const StorageAccessDenied_base: Schema.TaggedErrorClass<StorageAccessDenied, "StorageAccessDenied", {
517
+ readonly _tag: Schema.tag<"StorageAccessDenied">;
518
+ } & {
519
+ refId: typeof Schema.String;
520
+ reason: typeof Schema.String;
521
+ }>;
522
+
523
+ /**
524
+ * Storage failure. `transient` drives retry — `true` for 429 / 5xx /
525
+ * network blips, `false` for 4xx / config / not-found. Surfaced typed so
526
+ * handlers can `Effect.catchTag('StorageError', …)`.
527
+ */
528
+ export declare class StorageError extends StorageError_base {
529
+ }
530
+
531
+ declare const StorageError_base: Schema.TaggedErrorClass<StorageError, "StorageError", {
532
+ readonly _tag: Schema.tag<"StorageError">;
533
+ } & {
534
+ provider: typeof Schema.String;
535
+ message: typeof Schema.String;
536
+ transient: typeof Schema.Boolean;
537
+ status: Schema.optional<typeof Schema.Number>;
538
+ }>;
539
+
540
+ /** A persisted, explicit share of one object with one principal. */
541
+ export declare interface StorageGrant {
542
+ readonly id: string;
543
+ readonly refId: string;
544
+ readonly principalType: GrantPrincipalType;
545
+ readonly principalId: string;
546
+ readonly permission: GrantPermission;
547
+ readonly createdAt: Date;
548
+ readonly expiresAt: Date | null;
549
+ readonly createdBy: string | null;
550
+ }
551
+
552
+ /** Custom guard — resolves whether a subject may access a given ref. */
553
+ export declare type StorageGuard = (ctx: {
554
+ readonly subject: Subject;
555
+ readonly ref: StorageRef;
556
+ }) => boolean | Promise<boolean> | Effect.Effect<boolean>;
557
+
558
+ export declare const storagePlugin: (options?: StoragePluginOptions) => VoltroPlugin;
559
+
560
+ export declare interface StoragePluginOptions {
561
+ /** `'s3' | 'minio' | 'filesystem' | 'memory'` (built from env) or a
562
+ * `StorageProvider`. Default: `STORAGE_PROVIDER` env, else `'memory'`. */
563
+ readonly provider?: StorageProviderName | StorageProvider;
564
+ /** Bucket name (s3/minio). Default `S3_BUCKET`/`STORAGE_BUCKET` env, else 'voltro'. */
565
+ readonly bucket?: string;
566
+ readonly region?: string;
567
+ readonly endpoint?: string;
568
+ readonly accessKeyId?: string;
569
+ readonly secretAccessKey?: string;
570
+ /** Filesystem provider root. Default `STORAGE_ROOT` env, else '.voltro-storage'. */
571
+ readonly root?: string;
572
+ /** Azure storage account name (provider `'azure'`). Env `AZURE_STORAGE_ACCOUNT`. */
573
+ readonly accountName?: string;
574
+ /** Azure account key — needed to mint SAS URLs. Env `AZURE_STORAGE_KEY`. */
575
+ readonly accountKey?: string;
576
+ /** Azure connection string (alternative to account name/key). Env `AZURE_STORAGE_CONNECTION_STRING`. */
577
+ readonly connectionString?: string;
578
+ /** Public-object base URL (a CDN in front of the bucket, or an R2/GCS
579
+ * public domain). `${cdnBaseUrl}/${key}`. */
580
+ readonly cdnBaseUrl?: string;
581
+ /** Absolute public base URL of THIS api (`scheme://host[:port]`). Env
582
+ * `VOLTRO_PUBLIC_URL`. Prepended to the `/_voltro/storage/:id` serve URL so
583
+ * `getUrl`/`mintUrl`/the upload route return ABSOLUTE URLs — required when
584
+ * the api is a different origin than the app (browser `<img>` +
585
+ * server-to-server AI fetches). Omit for same-origin (relative URL). */
586
+ readonly publicBaseUrl?: string;
587
+ /** Origins allowed to read the serve route + POST the upload route
588
+ * cross-origin (CORS). Env `STORAGE_ALLOWED_ORIGINS` (comma-separated), or
589
+ * `'*'` for any. Omit for same-origin only. */
590
+ readonly allowedOrigins?: ReadonlyArray<string> | '*';
591
+ /** Normalize raster images on upload: apply EXIF orientation + strip ALL
592
+ * metadata (GPS/camera), re-encoding the bytes. Off by default. Width/height
593
+ * + LQIP placeholder are extracted regardless. */
594
+ readonly normalizeImages?: boolean;
595
+ /** Per-tenant quota enforced on upload (object count / total bytes). The
596
+ * headroom is consumed atomically (a CAS over the `_voltro_storage_usage`
597
+ * counter row) BEFORE the bytes are stored, so concurrent uploads across
598
+ * replicas cannot overshoot the cap. Exceeding it fails with
599
+ * `StorageRejected` (`'quota-exceeded'`). */
600
+ readonly quota?: {
601
+ readonly maxBytes?: number;
602
+ readonly maxCount?: number;
603
+ };
604
+ /** Send `ACL: public-read` on public puts (s3). Default true; set false
605
+ * for R2 / buckets that reject per-object ACLs. */
606
+ readonly publicAcl?: boolean;
607
+ /** Metadata store: a `RefStore` or `'memory'` (default). A DB-backed
608
+ * store is wired by the serve pipeline once the app's DataStore exists. */
609
+ readonly refStore?: 'memory' | RefStore;
610
+ /** Grant store: a `GrantStore` or `'memory'` (default). DB-backed once
611
+ * the DataStore is available, like `refStore`. */
612
+ readonly grantStore?: 'memory' | GrantStore;
613
+ /** Static key prefix prepended to every object (app/env namespace). */
614
+ readonly tenantPrefix?: string;
615
+ /** Transient-failure retries. Default 3. */
616
+ readonly attempts?: number;
617
+ /** Default presigned-URL / grant-token TTL (seconds). Default 3600. */
618
+ readonly urlExpiresInSec?: number;
619
+ /** Access engine config — a group resolver + named custom guards. */
620
+ readonly access?: AccessOptions;
621
+ /** Upload constraints — size cap, content-type allow-list, magic-byte sniff. */
622
+ readonly limits?: UploadLimits;
623
+ /** Virus scanner run on every `put` before bytes are stored (e.g.
624
+ * `clamavScanner({...})`). A detection fails with `StorageScanRejected`. */
625
+ readonly scan?: StorageScanner;
626
+ /** App-supplied probe for video/audio uploads — extracts duration + a poster
627
+ * frame (stored as the ref's `placeholder`). The core bundles no transcoder;
628
+ * wire ffprobe / a cloud API. Best-effort — never blocks an upload. */
629
+ readonly videoProbe?: VideoProbe;
630
+ /** Transcode step for video/audio — produces renditions (720p, webm, poster,
631
+ * …) stored as linked refs. Use the first-party `ffmpegTranscoder({...})` or
632
+ * bring your own. Runs in the background after upload. */
633
+ readonly transcode?: Transcoder;
634
+ /** Auto-run `transcode` after a video/audio upload. Default ON when a
635
+ * transcoder is set; false to only transcode via `service.transcode(refId)`. */
636
+ readonly autoTranscode?: boolean;
637
+ /** Disambiguates multiple instances of this plugin in one app. */
638
+ readonly name?: string;
639
+ }
640
+
641
+ /**
642
+ * A backend transport. Keyed by an opaque object key (the service owns key
643
+ * derivation + tenant prefixing); knows nothing about refs, tenants, or
644
+ * access policy.
645
+ */
646
+ export declare interface StorageProvider {
647
+ readonly name: string;
648
+ /** True when `getUrl` returns a real presigned URL (s3/minio). When
649
+ * false the service serves bytes via the `/_voltro/storage/:id`
650
+ * endpoint instead (filesystem/memory). */
651
+ readonly presigns: boolean;
652
+ readonly put: (key: string, bytes: Uint8Array, meta: {
653
+ readonly contentType: string;
654
+ readonly public?: boolean;
655
+ }) => Effect.Effect<ObjectMeta, StorageError>;
656
+ readonly get: (key: string) => Effect.Effect<StoredObject, StorageError>;
657
+ readonly delete: (key: string) => Effect.Effect<void, StorageError>;
658
+ /** Presigned GET (s3/minio) for private reads. */
659
+ readonly getUrl: (key: string, expiresInSec: number) => Effect.Effect<string, StorageError>;
660
+ /** A direct, cacheable URL for a PUBLIC object — virtual-hosted bucket
661
+ * URL or a configured CDN base. `null` when the provider can't serve
662
+ * publicly without the app (filesystem/memory) → the service falls
663
+ * back to the `/_voltro/storage/:id` route. */
664
+ readonly publicUrl?: (key: string) => string | null;
665
+ /** Presigned PUT for direct-to-bucket uploads (s3/minio). Absent on
666
+ * providers that can't presign uploads. */
667
+ readonly getUploadUrl?: (key: string, expiresInSec: number, contentType: string) => Effect.Effect<string, StorageError>;
668
+ readonly createMultipartUpload?: (key: string, contentType: string, opts?: {
669
+ readonly public?: boolean;
670
+ }) => Effect.Effect<{
671
+ readonly uploadId: string;
672
+ }, StorageError>;
673
+ readonly presignUploadPart?: (key: string, uploadId: string, partNumber: number, expiresInSec: number) => Effect.Effect<string, StorageError>;
674
+ readonly completeMultipartUpload?: (key: string, uploadId: string, parts: ReadonlyArray<{
675
+ readonly partNumber: number;
676
+ readonly etag: string;
677
+ }>) => Effect.Effect<{
678
+ readonly etag?: string;
679
+ }, StorageError>;
680
+ readonly abortMultipartUpload?: (key: string, uploadId: string) => Effect.Effect<void, StorageError>;
681
+ readonly head: (key: string) => Effect.Effect<ObjectMeta | null, StorageError>;
682
+ /**
683
+ * Stream an object's bytes in chunks instead of buffering the whole blob
684
+ * ({@link get}). Present on backends with a real streaming read (filesystem,
685
+ * s3, azure). OPTIONAL: when a provider omits it, {@link getObjectStream}
686
+ * transparently falls back to `get` + a chunked in-memory stream — correct
687
+ * for the small-blob backends (memory / database) that have nothing to
688
+ * stream from.
689
+ */
690
+ readonly getStream?: (key: string) => Effect.Effect<StoredStream, StorageError>;
691
+ /**
692
+ * Consume a {@link ByteStream} and store it via the backend's chunked /
693
+ * multipart upload, so a large upload never fully materialises in memory.
694
+ * Present on filesystem (streamed atomic write), s3 (multipart via
695
+ * `@aws-sdk/lib-storage`), azure (`uploadStream`). OPTIONAL: when absent,
696
+ * {@link putObjectStream} buffers the stream and calls `put`. `totalSize`,
697
+ * when the caller knows it (e.g. from an export manifest), lets the backend
698
+ * report an accurate size without a follow-up HEAD.
699
+ */
700
+ readonly putStream?: (key: string, stream: ByteStream, meta: {
701
+ readonly contentType: string;
702
+ readonly public?: boolean;
703
+ readonly totalSize?: number;
704
+ }) => Effect.Effect<ObjectMeta, StorageError>;
705
+ /**
706
+ * Read a byte range `[start, endInclusive]` (HTTP Range / fd offset). Enables
707
+ * seekable + resumable reads. OPTIONAL and best-effort: whole-object resume
708
+ * already works via content-addressing (skip an asset whose hash is present
709
+ * at the target), so this is an optimisation, not a correctness requirement.
710
+ */
711
+ readonly getRange?: (key: string, start: number, endInclusive: number) => Effect.Effect<Uint8Array, StorageError>;
712
+ /** Providers that store bytes via the app's DataStore (e.g. `database`)
713
+ * receive it here once it exists — bound by the plugin at boot. */
714
+ readonly bindDataStore?: (store: unknown) => void;
715
+ }
716
+
717
+ export declare type StorageProviderName = 's3' | 'minio' | 'azure' | 'filesystem' | 'memory' | 'database';
718
+
719
+ /** Per-tenant upload budget (see `StorageServiceOptions.quota`). */
720
+ export declare interface StorageQuota {
721
+ readonly maxBytes?: number;
722
+ readonly maxCount?: number;
723
+ }
724
+
725
+ /**
726
+ * Metadata row for a stored blob — the bytes live in object storage; this
727
+ * (in `_voltro_storage_refs`) is the DB-side handle. `tenantId` = the
728
+ * active organization id (D5). `passwordHash` is server-only and is never
729
+ * serialized to a client (stripped from inspect output + URLs).
730
+ */
731
+ export declare interface StorageRef {
732
+ readonly id: string;
733
+ readonly tenantId: string | null;
734
+ /** subject id that created the object (drives the `owner` rule). */
735
+ readonly ownerId: string | null;
736
+ readonly bucket: string;
737
+ readonly key: string;
738
+ readonly contentType: string;
739
+ readonly size: number;
740
+ readonly checksum: string;
741
+ readonly visibility: StorageVisibility;
742
+ readonly accessPolicy: AccessPolicy | null;
743
+ /** `salt:scryptHash` of the file password, or null. Server-only. */
744
+ readonly passwordHash: string | null;
745
+ readonly createdAt: Date;
746
+ /** Pixel width (images). */
747
+ readonly width?: number | null;
748
+ /** Pixel height (images). */
749
+ readonly height?: number | null;
750
+ /** Media duration in seconds (video/audio; populated by an app hook — the
751
+ * core does not bundle a transcoder). */
752
+ readonly duration?: number | null;
753
+ /** A tiny `data:image/webp;base64,…` LQIP placeholder (images). */
754
+ readonly placeholder?: string | null;
755
+ readonly folder?: string | null;
756
+ readonly tags?: ReadonlyArray<string> | null;
757
+ readonly alt?: string | null;
758
+ readonly caption?: string | null;
759
+ /** Parent ref id when this ref is a derivative; null on an original upload. */
760
+ readonly derivedFrom?: string | null;
761
+ /** Derivative label — e.g. `'rendition'` / `'poster'`; null on an original. */
762
+ readonly kind?: string | null;
763
+ }
764
+
765
+ /** An upload was rejected by a constraint (size / content-type / magic-byte
766
+ * mismatch). `reason` is a stable code; `detail` is human-readable. */
767
+ export declare class StorageRejected extends StorageRejected_base {
768
+ }
769
+
770
+ declare const StorageRejected_base: Schema.TaggedErrorClass<StorageRejected, "StorageRejected", {
771
+ readonly _tag: Schema.tag<"StorageRejected">;
772
+ } & {
773
+ /** `'too-large' | 'content-type-not-allowed' | 'content-mismatch'` */
774
+ reason: typeof Schema.String;
775
+ detail: typeof Schema.String;
776
+ }>;
777
+
778
+ /** Scans bytes before they're stored. Return `{ clean:false, threat }` to
779
+ * reject the upload (fails with `StorageScanRejected`, nothing is stored). */
780
+ export declare type StorageScanner = (input: {
781
+ readonly bytes: Uint8Array;
782
+ readonly contentType: string;
783
+ readonly key?: string;
784
+ }) => StorageScanResult | Promise<StorageScanResult> | Effect.Effect<StorageScanResult>;
785
+
786
+ /** An upload was rejected by the virus scanner. */
787
+ export declare class StorageScanRejected extends StorageScanRejected_base {
788
+ }
789
+
790
+ declare const StorageScanRejected_base: Schema.TaggedErrorClass<StorageScanRejected, "StorageScanRejected", {
791
+ readonly _tag: Schema.tag<"StorageScanRejected">;
792
+ } & {
793
+ threat: typeof Schema.String;
794
+ }>;
795
+
796
+ export declare interface StorageScanResult {
797
+ readonly clean: boolean;
798
+ /** Threat name when `clean` is false. */
799
+ readonly threat?: string;
800
+ }
801
+
802
+ export declare class StorageService extends StorageService_base {
803
+ }
804
+
805
+ declare const StorageService_base: Context.TagClass<StorageService, "@voltro/plugin-storage/StorageService", StorageServiceShape>;
806
+
807
+ export declare const storageServiceLayer: (options: StorageServiceOptions) => Layer.Layer<StorageService>;
808
+
809
+ export declare interface StorageServiceOptions {
810
+ readonly provider: StorageProvider;
811
+ readonly refStore: RefStore;
812
+ readonly grantStore: GrantStore;
813
+ readonly bucket: string;
814
+ /** Static prefix prepended to every key (e.g. an app/env namespace). */
815
+ readonly tenantPrefix?: string;
816
+ /** Transient-failure retries (429 / 5xx / network). Default 3. */
817
+ readonly attempts?: number;
818
+ /** Default presigned-URL / grant-token TTL (seconds). Default 3600. */
819
+ readonly urlExpiresInSec?: number;
820
+ /** Group resolver + custom guards for the access engine. */
821
+ readonly access?: AccessOptions;
822
+ /** Upload constraints — size cap, content-type allow-list, magic-byte sniff. */
823
+ readonly limits?: UploadLimits;
824
+ /** Virus scanner run on `put` before the bytes are stored. */
825
+ readonly scan?: StorageScanner;
826
+ /** App-supplied probe for video/audio uploads (duration + poster frame).
827
+ * The core bundles no transcoder; wire ffprobe / a cloud API. Best-effort. */
828
+ readonly videoProbe?: VideoProbe;
829
+ /** App-supplied (or first-party `ffmpegTranscoder`) transcode step producing
830
+ * renditions from video/audio uploads. */
831
+ readonly transcode?: Transcoder;
832
+ /** Auto-run `transcode` in the background after a video/audio upload. Defaults
833
+ * to ON when a transcoder is configured; set false to only transcode when the
834
+ * app explicitly calls `service.transcode(refId)` (e.g. from a durable job). */
835
+ readonly autoTranscode?: boolean;
836
+ /** Absolute public base URL of THIS api (`scheme://host[:port]`) — prepended
837
+ * to the `/_voltro/storage/:id` serve URL so `getUrl`/`mintUrl`/the upload
838
+ * route emit ABSOLUTE URLs. Required when the api is a different origin than
839
+ * the app: a browser `<img>` and server-to-server AI-gateway fetches both
840
+ * need an absolute, reachable URL. Omit for same-origin (relative URL). */
841
+ readonly publicBaseUrl?: string;
842
+ /** Normalize raster images on upload: apply EXIF orientation, then strip ALL
843
+ * metadata (GPS/camera) and re-encode. Off by default (it re-encodes, which
844
+ * changes the bytes + content-address). Width/height/placeholder are always
845
+ * extracted regardless. */
846
+ readonly normalizeImages?: boolean;
847
+ /** Per-tenant quota enforced on `put` (and multipart completion). The
848
+ * headroom is consumed ATOMICALLY (`RefStore.consumeUsage`) before the
849
+ * bytes are stored — concurrent uploads across replicas cannot overshoot
850
+ * the cap — and refunded if the write fails. Exceeding it fails with
851
+ * `StorageRejected` (reason `'quota-exceeded'`) before any bytes land. */
852
+ readonly quota?: StorageQuota;
853
+ }
854
+
855
+ /** The service handlers consume: `const storage = yield* StorageService`. */
856
+ export declare interface StorageServiceShape {
857
+ readonly put: (input: PutInput) => Effect.Effect<StorageRef, StorageError | StorageRejected | StorageScanRejected>;
858
+ /** Fetch an object's bytes by id. Enforces the tenant guard ONLY — it does
859
+ * NOT run the access policy / grant check. Use this for trusted server-side
860
+ * reads where the caller has already authorized the access; to deliver a
861
+ * PRIVATE object to an end user, go through `mintUrl` (access-checked) and
862
+ * the serve route, never `get` straight off a client-supplied id. */
863
+ readonly get: (id: string, opts?: {
864
+ readonly tenantId?: string | null;
865
+ }) => Effect.Effect<{
866
+ readonly bytes: Uint8Array;
867
+ readonly ref: StorageRef;
868
+ }, StorageError>;
869
+ /** Direct/presigned URL for an object. For PUBLIC objects this is the
870
+ * CDN/bucket URL (no app round-trip).
871
+ *
872
+ * UNCHECKED for PRIVATE objects: it enforces the tenant guard but NOT the
873
+ * access policy / grants, and on a presigning provider (s3/minio) returns a
874
+ * live presigned GET — i.e. a working, ungated download URL. To hand a
875
+ * private object to an end user, use `mintUrl` (it runs `checkAccess`
876
+ * first). Reach for `getUrl` on a private ref only when the caller has
877
+ * already authorized the access itself. */
878
+ readonly getUrl: (id: string, opts?: {
879
+ readonly tenantId?: string | null;
880
+ readonly expiresInSec?: number;
881
+ }) => Effect.Effect<string, StorageError>;
882
+ readonly head: (id: string, opts?: {
883
+ readonly tenantId?: string | null;
884
+ }) => Effect.Effect<StorageRef | null, StorageError>;
885
+ /** Read a byte range `[start, endInclusive]` of an object (HTTP Range / video
886
+ * seeking / memory-bounded delivery). Uses the provider's native `getRange`
887
+ * when present (filesystem / s3 / azure) and otherwise falls back to a
888
+ * buffered `get` + slice (memory / database — small blobs, nothing to seek).
889
+ * Clamps `endInclusive` to the object's last byte. Enforces the tenant guard
890
+ * ONLY, like `get` — the serve route runs the access check before calling it.
891
+ * Fails `StorageError` status 416 for a start past the end of the object. */
892
+ readonly getRange: (id: string, range: {
893
+ readonly start: number;
894
+ readonly endInclusive: number;
895
+ }, opts?: {
896
+ readonly tenantId?: string | null;
897
+ }) => Effect.Effect<{
898
+ readonly bytes: Uint8Array;
899
+ readonly ref: StorageRef;
900
+ readonly totalSize: number;
901
+ }, StorageError>;
902
+ readonly delete: (id: string, opts?: {
903
+ readonly tenantId?: string | null;
904
+ }) => Effect.Effect<void, StorageError>;
905
+ /** Run the access check for `subject`, then return a URL that delivers
906
+ * the bytes: a public/CDN URL, a presigned GET (s3/minio), or an app
907
+ * serve URL carrying a short-lived signed grant token (fs/memory). */
908
+ readonly mintUrl: (id: string, subject: Subject, opts?: {
909
+ readonly password?: string;
910
+ readonly expiresInSec?: number;
911
+ }) => Effect.Effect<string, StorageError | StorageAccessDenied>;
912
+ /** Presigned PUT for direct-to-bucket upload (s3/minio). */
913
+ readonly mintUploadUrl: (input: {
914
+ readonly key: string;
915
+ readonly contentType: string;
916
+ readonly tenantId?: string | null;
917
+ readonly expiresInSec?: number;
918
+ }) => Effect.Effect<string, StorageError>;
919
+ /** Whether `subject` may access `ref` (policy + persisted grants). */
920
+ readonly checkAccess: (ref: StorageRef, subject: Subject, opts?: {
921
+ readonly password?: string;
922
+ readonly permission?: GrantPermission;
923
+ }) => Effect.Effect<boolean, StorageError>;
924
+ /** List / browse / search the stored refs by `folder` prefix, `tags`,
925
+ * `ownerId`, and `tenantId`, newest-first, with offset paging. The
926
+ * first-class media-library / file-manager read — a consumer never has to
927
+ * query `_voltro_storage_refs` by hand. Enforces NO per-ref access policy
928
+ * (it filters the index, it doesn't deliver bytes); scope it to the
929
+ * caller's `tenantId` / `ownerId` for an end-user surface, and deliver any
930
+ * listed private object through `mintUrl`. */
931
+ readonly listRefs: (input?: ListRefsInput) => Effect.Effect<ListRefsResult, StorageError>;
932
+ readonly grant: (input: GrantInput) => Effect.Effect<StorageGrant, StorageError>;
933
+ readonly revoke: (grantId: string) => Effect.Effect<void, StorageError>;
934
+ readonly listGrants: (refId: string) => Effect.Effect<ReadonlyArray<StorageGrant>, StorageError>;
935
+ /** Bytes + object count for a tenant (drives per-tenant quotas + dashboards). */
936
+ readonly usage: (tenantId: string | null) => Effect.Effect<{
937
+ readonly bytes: number;
938
+ readonly count: number;
939
+ }, StorageError>;
940
+ /** Garbage-collect dangling refs whose bytes are gone from the provider
941
+ * (crash between put + insert, or a manual blob delete). Scans up to
942
+ * `limit` recent refs; run repeatedly for a full sweep. `dryRun` reports
943
+ * without deleting. */
944
+ readonly sweepOrphans: (opts?: {
945
+ readonly limit?: number;
946
+ readonly dryRun?: boolean;
947
+ }) => Effect.Effect<{
948
+ readonly scanned: number;
949
+ readonly removed: ReadonlyArray<string>;
950
+ }, StorageError>;
951
+ /** Fetch an external URL SERVER-SIDE and store it as an asset (one-liner CMS
952
+ * migration / "adopt this remote image"). The content-type comes from the
953
+ * response unless overridden. NOTE: this fetches an arbitrary URL from the
954
+ * server — pass only trusted URLs (SSRF); front it with an allow-list if the
955
+ * URL is user-supplied. */
956
+ readonly ingestUrl: (url: string, opts?: {
957
+ readonly contentType?: string;
958
+ readonly tenantId?: string | null;
959
+ readonly ownerId?: string | null;
960
+ readonly visibility?: StorageVisibility;
961
+ readonly key?: string;
962
+ readonly folder?: string;
963
+ readonly tags?: ReadonlyArray<string>;
964
+ readonly alt?: string;
965
+ readonly caption?: string;
966
+ }) => Effect.Effect<StorageRef, StorageError | StorageRejected | StorageScanRejected>;
967
+ /** Mint a presigned direct-to-bucket PUT for a fresh random TEMP key
968
+ * (`_incoming/…`). Returns the URL to PUT raw bytes to + the derived key to
969
+ * hand back to `finalizeUpload`. Only for presigning providers (s3/minio);
970
+ * fails otherwise so the caller can fall back to the through-app route. */
971
+ readonly mintPresignedUpload: (input: {
972
+ readonly contentType: string;
973
+ readonly tenantId?: string | null;
974
+ readonly expiresInSec?: number;
975
+ }) => Effect.Effect<{
976
+ readonly uploadUrl: string;
977
+ readonly key: string;
978
+ readonly expiresInSec: number;
979
+ }, StorageError>;
980
+ /** Promote a blob the client PUT directly to a TEMP `key` into a real ref:
981
+ * fetch it back, run the FULL `put()` pipeline (scan/checksum/derivatives/
982
+ * quota), then delete the temp key. This is where a presigned upload gets
983
+ * scanned + a non-degraded ref — out of band from the direct PUT. On a scan
984
+ * reject the temp bytes are deleted (fail-closed, nothing promoted). */
985
+ readonly finalizeUpload: (input: {
986
+ readonly key: string;
987
+ readonly contentType: string;
988
+ readonly tenantId?: string | null;
989
+ readonly ownerId?: string | null;
990
+ readonly visibility?: StorageVisibility;
991
+ readonly folder?: string;
992
+ readonly tags?: ReadonlyArray<string>;
993
+ readonly alt?: string;
994
+ readonly caption?: string;
995
+ }) => Effect.Effect<StorageRef, StorageError | StorageRejected | StorageScanRejected>;
996
+ /** Store one chunk of a resumable upload as its own durable `_incoming/<id>/
997
+ * <index>` object. Idempotent — re-PUTting the same index overwrites it. */
998
+ readonly putResumableChunk: (input: {
999
+ readonly uploadId: string;
1000
+ readonly index: number;
1001
+ readonly bytes: Uint8Array;
1002
+ readonly tenantId?: string | null;
1003
+ }) => Effect.Effect<void, StorageError>;
1004
+ /** How far a resumable upload got: probes contiguous chunk objects from index
1005
+ * 0 and returns the received byte `offset` + the `nextIndex` to send. Lets a
1006
+ * reconnecting client resume instead of restarting. */
1007
+ readonly resumableStatus: (input: {
1008
+ readonly uploadId: string;
1009
+ readonly tenantId?: string | null;
1010
+ }) => Effect.Effect<{
1011
+ readonly offset: number;
1012
+ readonly nextIndex: number;
1013
+ }, StorageError>;
1014
+ /** Assemble a completed resumable upload: read chunks `0..count-1` in order,
1015
+ * concatenate, run the FULL `put()` pipeline, then delete the chunk objects
1016
+ * (on success AND on reject — fail-closed). */
1017
+ readonly finalizeResumable: (input: {
1018
+ readonly uploadId: string;
1019
+ readonly count: number;
1020
+ readonly contentType: string;
1021
+ readonly tenantId?: string | null;
1022
+ readonly ownerId?: string | null;
1023
+ readonly visibility?: StorageVisibility;
1024
+ readonly folder?: string;
1025
+ readonly tags?: ReadonlyArray<string>;
1026
+ readonly alt?: string;
1027
+ readonly caption?: string;
1028
+ }) => Effect.Effect<StorageRef, StorageError | StorageRejected | StorageScanRejected>;
1029
+ /** Run the configured transcoder over a stored asset and persist each output
1030
+ * as a rendition ref (linked via `derivedFrom`). Idempotent-ish: re-running
1031
+ * content-addresses identical outputs. No-op (→ `[]`) when no transcoder is
1032
+ * configured or the ref is itself a derivative. */
1033
+ readonly transcode: (refId: string, opts?: {
1034
+ readonly tenantId?: string | null;
1035
+ }) => Effect.Effect<ReadonlyArray<StorageRef>, StorageError | StorageRejected | StorageScanRejected>;
1036
+ /** List the derivative refs (renditions, posters) produced from `refId`. */
1037
+ readonly renditions: (refId: string, opts?: {
1038
+ readonly tenantId?: string | null;
1039
+ }) => Effect.Effect<ReadonlyArray<StorageRef>, StorageError>;
1040
+ /** Begin a multipart session on a presigning provider (s3/minio). Returns the
1041
+ * bucket `key` + provider `uploadId` to sign parts against. Fails when the
1042
+ * provider can't do multipart (the client falls back to `resumable`). */
1043
+ readonly beginMultipartUpload: (input: {
1044
+ readonly contentType: string;
1045
+ readonly tenantId?: string | null;
1046
+ readonly visibility?: StorageVisibility;
1047
+ readonly expiresInSec?: number;
1048
+ }) => Effect.Effect<{
1049
+ readonly uploadId: string;
1050
+ readonly key: string;
1051
+ }, StorageError>;
1052
+ /** Presign a PUT for one part (1-based `partNumber`). */
1053
+ readonly signMultipartPart: (input: {
1054
+ readonly key: string;
1055
+ readonly uploadId: string;
1056
+ readonly partNumber: number;
1057
+ readonly expiresInSec?: number;
1058
+ }) => Effect.Effect<string, StorageError>;
1059
+ /** Complete a multipart session → the bucket assembles the object; a ref is
1060
+ * registered straight from it (size via HEAD, etag as checksum). No fetch-back,
1061
+ * so NO scan/derivatives — scan multi-GB out of band. */
1062
+ readonly completeMultipart: (input: {
1063
+ readonly key: string;
1064
+ readonly uploadId: string;
1065
+ readonly parts: ReadonlyArray<{
1066
+ readonly partNumber: number;
1067
+ readonly etag: string;
1068
+ }>;
1069
+ readonly contentType: string;
1070
+ readonly tenantId?: string | null;
1071
+ readonly ownerId?: string | null;
1072
+ readonly visibility?: StorageVisibility;
1073
+ readonly folder?: string;
1074
+ readonly tags?: ReadonlyArray<string>;
1075
+ readonly alt?: string;
1076
+ readonly caption?: string;
1077
+ }) => Effect.Effect<StorageRef, StorageError | StorageRejected>;
1078
+ /** Abort a multipart session (client cancelled) — frees the bucket's parts. */
1079
+ readonly abortMultipart: (input: {
1080
+ readonly key: string;
1081
+ readonly uploadId: string;
1082
+ }) => Effect.Effect<void, StorageError>;
1083
+ }
1084
+
1085
+ /** `public` = anyone, served direct from bucket/CDN; `private` = gated by
1086
+ * the ref's access policy + grants. Default for a new put is `private`. */
1087
+ export declare type StorageVisibility = 'public' | 'private';
1088
+
1089
+ /** Bytes + content type as returned by a provider `get`. */
1090
+ export declare interface StoredObject {
1091
+ readonly bytes: Uint8Array;
1092
+ readonly contentType: string;
1093
+ }
1094
+
1095
+ /** A streamed read from a provider — the object as a {@link ByteStream} plus
1096
+ * its content type and, when the backend reports it up front, its size. */
1097
+ export declare interface StoredStream {
1098
+ readonly stream: ByteStream;
1099
+ readonly contentType: string;
1100
+ readonly size?: number;
1101
+ }
1102
+
1103
+ /**
1104
+ * A ByteStream → Node `Readable`, for SDK uploaders that consume Node streams
1105
+ * (s3 `Upload`, azure `uploadStream`) or `stream.pipeline` to a file. Bridges
1106
+ * via a Web ReadableStream, which Effect produces natively.
1107
+ */
1108
+ export declare const toNodeReadable: (stream: ByteStream) => Promise<Readable>;
1109
+
1110
+ /**
1111
+ * App-supplied (or first-party `ffmpegTranscoder`) transcode step. Given a
1112
+ * source asset's bytes, returns zero or more renditions (720p mp4, webm, HLS,
1113
+ * a poster, …). The core bundles no transcoder — wire ffmpeg (or a cloud API).
1114
+ * Runs OUT OF BAND (never blocks `put()`); best-effort — a throw/failure/[] just
1115
+ * means no renditions.
1116
+ */
1117
+ export declare type Transcoder = (input: {
1118
+ readonly bytes: Uint8Array;
1119
+ readonly contentType: string;
1120
+ readonly ref: StorageRef;
1121
+ }) => ReadonlyArray<Rendition> | Promise<ReadonlyArray<Rendition>> | Effect.Effect<ReadonlyArray<Rendition>>;
1122
+
1123
+ /** Transform with a small FIFO cache keyed by `(refId, params)` so repeated
1124
+ * requests for the same variant don't re-encode. */
1125
+ export declare const transformCached: (refId: string, bytes: Uint8Array, contentType: string, params: TransformParams) => Promise<Transformed | null>;
1126
+
1127
+ export declare interface Transformed {
1128
+ readonly bytes: Uint8Array;
1129
+ readonly contentType: string;
1130
+ }
1131
+
1132
+ /**
1133
+ * Transform image bytes per `params`. Returns null (→ serve original) when
1134
+ * the content isn't a raster image, `sharp` isn't installed, or processing
1135
+ * fails (graceful — a bad transform should never 500 a valid object).
1136
+ */
1137
+ export declare const transformImage: (bytes: Uint8Array, contentType: string, params: TransformParams) => Promise<Transformed | null>;
1138
+
1139
+ export declare interface TransformParams {
1140
+ readonly w?: number;
1141
+ readonly h?: number;
1142
+ readonly format?: string;
1143
+ readonly q?: number;
1144
+ readonly fit?: string;
1145
+ }
1146
+
1147
+ export declare interface UploadLimits {
1148
+ /** Reject uploads larger than this many bytes. */
1149
+ readonly maxBytes?: number;
1150
+ /** Allowed content types — exact (`application/pdf`) or wildcard
1151
+ * (`image/*`). Omit to allow any. */
1152
+ readonly allowedContentTypes?: ReadonlyArray<string>;
1153
+ /** Verify the bytes' magic number matches the declared `contentType`
1154
+ * (blocks e.g. an executable uploaded as `image/png`). Default false. */
1155
+ readonly sniff?: boolean;
1156
+ }
1157
+
1158
+ /** What an upload ticket authorizes: a single through-app upload bound to an
1159
+ * owner/tenant/content-type/visibility, until `exp`. */
1160
+ export declare interface UploadTicketPayload {
1161
+ /** ownerId (subject id) the stored object is attributed to. */
1162
+ readonly sub: string | null;
1163
+ /** tenantId that scopes the key prefix + ref row. */
1164
+ readonly tenant: string | null;
1165
+ /** Authorized content-type — the stored object uses this. */
1166
+ readonly ct: string;
1167
+ readonly vis: 'public' | 'private';
1168
+ /** Optional fixed logical key (else content-addressed). */
1169
+ readonly key?: string;
1170
+ /** Max bytes the route will accept for this ticket (0/undefined = no cap). */
1171
+ readonly max?: number;
1172
+ /** App metadata carried onto the stored ref (so apps need no parallel table). */
1173
+ readonly folder?: string;
1174
+ readonly tags?: ReadonlyArray<string>;
1175
+ readonly alt?: string;
1176
+ readonly caption?: string;
1177
+ /** Unix seconds when the ticket expires. */
1178
+ readonly exp: number;
1179
+ }
1180
+
1181
+ /** One reservation's worth of usage — the object's stored size + one ref. */
1182
+ export declare interface UsageDelta {
1183
+ readonly bytes: number;
1184
+ readonly count: number;
1185
+ }
1186
+
1187
+ /** Verify a finalize token. Returns the payload on success, null on any failure
1188
+ * (malformed, bad signature, expired, missing temp key). Never throws. */
1189
+ export declare const verifyFinalizeTicket: (token: string, secret?: string) => FinalizeTicketPayload | null;
1190
+
1191
+ /**
1192
+ * Verify a grant token. Returns the payload on success, null on any
1193
+ * failure (malformed, bad signature, expired). Never throws.
1194
+ */
1195
+ export declare const verifyGrant: (token: string, secret?: string) => GrantTokenPayload | null;
1196
+
1197
+ /** Verify a multipart ticket. Returns the payload on success, null on any
1198
+ * failure (malformed, bad signature, expired, missing key/uploadId). */
1199
+ export declare const verifyMultipartTicket: (token: string, secret?: string) => MultipartTicketPayload | null;
1200
+
1201
+ /** Constant-time check of a plaintext password against a stored hash. */
1202
+ export declare const verifyPassword: (password: string | undefined, stored: string | null) => boolean;
1203
+
1204
+ /** Verify a resumable ticket. Returns the payload on success, null on any
1205
+ * failure (malformed, bad signature, expired, missing id/size). Never throws. */
1206
+ export declare const verifyResumableTicket: (token: string, secret?: string) => ResumableTicketPayload | null;
1207
+
1208
+ /** Verify an upload ticket. Returns the payload on success, null on any
1209
+ * failure (malformed, bad signature, expired). Never throws. */
1210
+ export declare const verifyUploadTicket: (token: string, secret?: string) => UploadTicketPayload | null;
1211
+
1212
+ /**
1213
+ * App-supplied media probe for video/audio uploads. The core deliberately does
1214
+ * NOT bundle ffmpeg/ffprobe (a large binary + licensing) — wire your own
1215
+ * (ffprobe, a cloud video API) and the service calls it in `put()` for
1216
+ * video/audio content types. Best-effort: a null/throw never blocks the upload.
1217
+ */
1218
+ export declare type VideoProbe = (input: {
1219
+ readonly bytes: Uint8Array;
1220
+ readonly contentType: string;
1221
+ }) => VideoProbeResult | null | Promise<VideoProbeResult | null> | Effect.Effect<VideoProbeResult | null>;
1222
+
1223
+ /** What a `VideoProbe` returns for a video/audio upload. All optional — return
1224
+ * `null` (or throw) and the upload proceeds with no media metadata. */
1225
+ export declare interface VideoProbeResult {
1226
+ /** Duration in seconds. */
1227
+ readonly duration?: number;
1228
+ readonly width?: number;
1229
+ readonly height?: number;
1230
+ /** A poster frame as a `data:image/…;base64,…` URI — stored as the ref's
1231
+ * `placeholder` (so a `<video poster>` / grid preview has an instant image). */
1232
+ readonly poster?: string;
1233
+ }
1234
+
1235
+ export declare const _voltroStorageBlobsTable: TableLike;
1236
+
1237
+ export declare const _voltroStorageGrantsTable: TableLike;
1238
+
1239
+ export declare const _voltroStorageRefsTable: TableLike;
1240
+
1241
+ export declare const _voltroStorageUsageTable: TableLike;
1242
+
1243
+ /**
1244
+ * Convenience mixin adding a single `asset` column (`assetRef()`) to a table,
1245
+ * applied like any other mixin via `.with(...)`:
1246
+ *
1247
+ * const posts = table('posts', { id: id() }).with(withStorage())
1248
+ *
1249
+ * Prefer `assetRef()` inline when you want a custom column name (`avatar`,
1250
+ * `cover`, …) or several asset columns; `withStorage()` is the one-column
1251
+ * shorthand. Follows the `tenant()` / `audit()` factory convention.
1252
+ */
1253
+ export declare const withStorage: () => ReturnType<typeof mixin>;
1254
+
1255
+ export { }