@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,646 @@
1
+ import { Context } from 'effect';
2
+ import { Effect } from 'effect';
3
+ import { Schema } from 'effect';
4
+ import { Stream } from 'effect';
5
+ import { Subject } from '@voltro/protocol';
6
+
7
+ export declare type AccessPolicy = ReadonlyArray<AccessRule>;
8
+
9
+ /**
10
+ * One access rule. A rule GRANTS when EVERY present condition is satisfied
11
+ * (AND). A policy is an array of rules; access is allowed when ANY rule
12
+ * grants (OR). An empty policy on a private object means owner-only (the
13
+ * implicit owner check always applies).
14
+ */
15
+ export declare interface AccessRule {
16
+ /** `subject.id === ref.ownerId`. */
17
+ readonly owner?: boolean;
18
+ /** `subject.metadata.roles` intersects these. */
19
+ readonly roles?: ReadonlyArray<string>;
20
+ /** resolved groups (default `subject.metadata.groups`) intersect these. */
21
+ readonly groups?: ReadonlyArray<string>;
22
+ /** `subject.scopes` intersects these. */
23
+ readonly scopes?: ReadonlyArray<string>;
24
+ /** `subject.tenantId === ref.tenantId`. */
25
+ readonly tenant?: boolean;
26
+ /** subject is any authenticated api-key. */
27
+ readonly apiKey?: boolean;
28
+ /** a correct password was supplied (checked at mint, against passwordHash). */
29
+ readonly password?: boolean;
30
+ /** a named custom guard (`options.guards[name]`) returns true. */
31
+ readonly guard?: string;
32
+ }
33
+
34
+ /**
35
+ * A lazy stream of bytes — the streaming counterpart to {@link StoredObject}'s
36
+ * eager `bytes`. Backpressured: chunks are pulled on demand, so a multi-GB blob
37
+ * never fully materialises in memory. The error channel is {@link StorageError}
38
+ * so a stream handed to `putStream` carries the same typed failure the rest of
39
+ * the transport does (callers map their source errors into it).
40
+ */
41
+ export declare type ByteStream = Stream.Stream<Uint8Array, StorageError>;
42
+
43
+ /** Input to `StorageService.grant`. */
44
+ export declare interface GrantInput {
45
+ readonly refId: string;
46
+ readonly principalType: GrantPrincipalType;
47
+ readonly principalId: string;
48
+ readonly permission?: GrantPermission;
49
+ readonly expiresAt?: Date | null;
50
+ readonly createdBy?: string | null;
51
+ }
52
+
53
+ export declare type GrantPermission = 'read' | 'write';
54
+
55
+ export declare type GrantPrincipalType = 'user' | 'group' | 'apiKey';
56
+
57
+ /**
58
+ * Filter + page a `StorageService.listRefs` / media-library query. Every
59
+ * filter is optional and ANDed; omit them all to page the whole (tenant-scoped)
60
+ * ref set newest-first.
61
+ */
62
+ export declare interface ListRefsInput {
63
+ /** Scope to one tenant (`null` = the system/global partition). Omit to span
64
+ * every tenant — reserve that for trusted admin reads, not an end-user
65
+ * media library. */
66
+ readonly tenantId?: string | null;
67
+ /** Match refs whose `folder` equals this value OR sits UNDER it as a path
68
+ * prefix (`photos` matches `photos` and `photos/2026`). A trailing slash is
69
+ * normalized away. */
70
+ readonly folder?: string;
71
+ /** Match refs whose `tags` contain ALL of these (AND). */
72
+ readonly tags?: ReadonlyArray<string>;
73
+ /** Match refs created by this subject (the `ownerId` column). */
74
+ readonly ownerId?: string;
75
+ /** Only originals (`false`, the default — excludes transcode renditions /
76
+ * posters) or include derivatives too (`true`). */
77
+ readonly includeDerived?: boolean;
78
+ /** Page size. Clamped to `[1, 500]`; default 50. */
79
+ readonly limit?: number;
80
+ /** Rows to skip (offset paging). Default 0. */
81
+ readonly offset?: number;
82
+ }
83
+
84
+ /** One page of a `listRefs` query. `nextOffset` is non-null when another page
85
+ * exists (i.e. the store returned a full `limit + 1` probe) — pass it back as
86
+ * the next `offset`. */
87
+ export declare interface ListRefsResult {
88
+ readonly refs: ReadonlyArray<StorageRef>;
89
+ readonly nextOffset: number | null;
90
+ }
91
+
92
+ /** Result of a provider `put` / `head`. */
93
+ export declare interface ObjectMeta {
94
+ readonly size: number;
95
+ readonly etag?: string;
96
+ }
97
+
98
+ /** A ref with the server-only `passwordHash` removed — safe to serialize. */
99
+ export declare type PublicStorageRef = Omit<StorageRef, 'passwordHash'>;
100
+
101
+ /** Input to `StorageService.put`. */
102
+ export declare interface PutInput {
103
+ readonly bytes: Uint8Array;
104
+ readonly contentType: string;
105
+ /** Active org id — scopes the key prefix + the ref row. */
106
+ readonly tenantId?: string | null;
107
+ /** Subject id that owns the object (enables the `owner` access rule). */
108
+ readonly ownerId?: string | null;
109
+ /** Optional logical key suffix (e.g. `avatars/<userId>/x.png`); else a
110
+ * content-addressed key is derived from the checksum. */
111
+ readonly key?: string;
112
+ /** `private` (default) or `public`. */
113
+ readonly visibility?: StorageVisibility;
114
+ /** Access rules for a private object (in addition to the owner check). */
115
+ readonly access?: AccessPolicy;
116
+ /** Plaintext password — hashed by the service, never stored as-is. */
117
+ readonly password?: string;
118
+ readonly folder?: string;
119
+ readonly tags?: ReadonlyArray<string>;
120
+ readonly alt?: string;
121
+ readonly caption?: string;
122
+ /** Media duration in seconds (caller-supplied for video/audio). */
123
+ readonly duration?: number;
124
+ /** Parent ref id — set when storing a derived rendition/poster (internal). */
125
+ readonly derivedFrom?: string;
126
+ /** Derivative label — `'rendition'` / `'poster'` (internal). */
127
+ readonly kind?: string;
128
+ }
129
+
130
+ /** One derived output a `Transcoder` produces from a source asset. Each becomes
131
+ * a normal storage ref linked back to the original via `derivedFrom`. */
132
+ export declare interface Rendition {
133
+ readonly bytes: Uint8Array;
134
+ readonly contentType: string;
135
+ /** Derivative class — e.g. `'rendition'` (a playable variant) or `'poster'`. */
136
+ readonly kind: string;
137
+ /** Human label stored as the ref's `caption` — e.g. `'720p'`, `'webm'`. */
138
+ readonly label?: string;
139
+ }
140
+
141
+ /**
142
+ * Access to a private object was refused. Distinct from `StorageError` so
143
+ * handlers can tell "not allowed" (403) from "backend blew up" (5xx) and
144
+ * the wire-error union carries it typed.
145
+ */
146
+ export declare class StorageAccessDenied extends StorageAccessDenied_base {
147
+ }
148
+
149
+ declare const StorageAccessDenied_base: Schema.TaggedErrorClass<StorageAccessDenied, "StorageAccessDenied", {
150
+ readonly _tag: Schema.tag<"StorageAccessDenied">;
151
+ } & {
152
+ refId: typeof Schema.String;
153
+ reason: typeof Schema.String;
154
+ }>;
155
+
156
+ /**
157
+ * Storage failure. `transient` drives retry — `true` for 429 / 5xx /
158
+ * network blips, `false` for 4xx / config / not-found. Surfaced typed so
159
+ * handlers can `Effect.catchTag('StorageError', …)`.
160
+ */
161
+ export declare class StorageError extends StorageError_base {
162
+ }
163
+
164
+ declare const StorageError_base: Schema.TaggedErrorClass<StorageError, "StorageError", {
165
+ readonly _tag: Schema.tag<"StorageError">;
166
+ } & {
167
+ provider: typeof Schema.String;
168
+ message: typeof Schema.String;
169
+ transient: typeof Schema.Boolean;
170
+ status: Schema.optional<typeof Schema.Number>;
171
+ }>;
172
+
173
+ /** A persisted, explicit share of one object with one principal. */
174
+ export declare interface StorageGrant {
175
+ readonly id: string;
176
+ readonly refId: string;
177
+ readonly principalType: GrantPrincipalType;
178
+ readonly principalId: string;
179
+ readonly permission: GrantPermission;
180
+ readonly createdAt: Date;
181
+ readonly expiresAt: Date | null;
182
+ readonly createdBy: string | null;
183
+ }
184
+
185
+ /** Custom guard — resolves whether a subject may access a given ref. */
186
+ export declare type StorageGuard = (ctx: {
187
+ readonly subject: Subject;
188
+ readonly ref: StorageRef;
189
+ }) => boolean | Promise<boolean> | Effect.Effect<boolean>;
190
+
191
+ /**
192
+ * A backend transport. Keyed by an opaque object key (the service owns key
193
+ * derivation + tenant prefixing); knows nothing about refs, tenants, or
194
+ * access policy.
195
+ */
196
+ export declare interface StorageProvider {
197
+ readonly name: string;
198
+ /** True when `getUrl` returns a real presigned URL (s3/minio). When
199
+ * false the service serves bytes via the `/_voltro/storage/:id`
200
+ * endpoint instead (filesystem/memory). */
201
+ readonly presigns: boolean;
202
+ readonly put: (key: string, bytes: Uint8Array, meta: {
203
+ readonly contentType: string;
204
+ readonly public?: boolean;
205
+ }) => Effect.Effect<ObjectMeta, StorageError>;
206
+ readonly get: (key: string) => Effect.Effect<StoredObject, StorageError>;
207
+ readonly delete: (key: string) => Effect.Effect<void, StorageError>;
208
+ /** Presigned GET (s3/minio) for private reads. */
209
+ readonly getUrl: (key: string, expiresInSec: number) => Effect.Effect<string, StorageError>;
210
+ /** A direct, cacheable URL for a PUBLIC object — virtual-hosted bucket
211
+ * URL or a configured CDN base. `null` when the provider can't serve
212
+ * publicly without the app (filesystem/memory) → the service falls
213
+ * back to the `/_voltro/storage/:id` route. */
214
+ readonly publicUrl?: (key: string) => string | null;
215
+ /** Presigned PUT for direct-to-bucket uploads (s3/minio). Absent on
216
+ * providers that can't presign uploads. */
217
+ readonly getUploadUrl?: (key: string, expiresInSec: number, contentType: string) => Effect.Effect<string, StorageError>;
218
+ readonly createMultipartUpload?: (key: string, contentType: string, opts?: {
219
+ readonly public?: boolean;
220
+ }) => Effect.Effect<{
221
+ readonly uploadId: string;
222
+ }, StorageError>;
223
+ readonly presignUploadPart?: (key: string, uploadId: string, partNumber: number, expiresInSec: number) => Effect.Effect<string, StorageError>;
224
+ readonly completeMultipartUpload?: (key: string, uploadId: string, parts: ReadonlyArray<{
225
+ readonly partNumber: number;
226
+ readonly etag: string;
227
+ }>) => Effect.Effect<{
228
+ readonly etag?: string;
229
+ }, StorageError>;
230
+ readonly abortMultipartUpload?: (key: string, uploadId: string) => Effect.Effect<void, StorageError>;
231
+ readonly head: (key: string) => Effect.Effect<ObjectMeta | null, StorageError>;
232
+ /**
233
+ * Stream an object's bytes in chunks instead of buffering the whole blob
234
+ * ({@link get}). Present on backends with a real streaming read (filesystem,
235
+ * s3, azure). OPTIONAL: when a provider omits it, {@link getObjectStream}
236
+ * transparently falls back to `get` + a chunked in-memory stream — correct
237
+ * for the small-blob backends (memory / database) that have nothing to
238
+ * stream from.
239
+ */
240
+ readonly getStream?: (key: string) => Effect.Effect<StoredStream, StorageError>;
241
+ /**
242
+ * Consume a {@link ByteStream} and store it via the backend's chunked /
243
+ * multipart upload, so a large upload never fully materialises in memory.
244
+ * Present on filesystem (streamed atomic write), s3 (multipart via
245
+ * `@aws-sdk/lib-storage`), azure (`uploadStream`). OPTIONAL: when absent,
246
+ * {@link putObjectStream} buffers the stream and calls `put`. `totalSize`,
247
+ * when the caller knows it (e.g. from an export manifest), lets the backend
248
+ * report an accurate size without a follow-up HEAD.
249
+ */
250
+ readonly putStream?: (key: string, stream: ByteStream, meta: {
251
+ readonly contentType: string;
252
+ readonly public?: boolean;
253
+ readonly totalSize?: number;
254
+ }) => Effect.Effect<ObjectMeta, StorageError>;
255
+ /**
256
+ * Read a byte range `[start, endInclusive]` (HTTP Range / fd offset). Enables
257
+ * seekable + resumable reads. OPTIONAL and best-effort: whole-object resume
258
+ * already works via content-addressing (skip an asset whose hash is present
259
+ * at the target), so this is an optimisation, not a correctness requirement.
260
+ */
261
+ readonly getRange?: (key: string, start: number, endInclusive: number) => Effect.Effect<Uint8Array, StorageError>;
262
+ /** Providers that store bytes via the app's DataStore (e.g. `database`)
263
+ * receive it here once it exists — bound by the plugin at boot. */
264
+ readonly bindDataStore?: (store: unknown) => void;
265
+ }
266
+
267
+ export declare type StorageProviderName = 's3' | 'minio' | 'azure' | 'filesystem' | 'memory' | 'database';
268
+
269
+ /**
270
+ * Metadata row for a stored blob — the bytes live in object storage; this
271
+ * (in `_voltro_storage_refs`) is the DB-side handle. `tenantId` = the
272
+ * active organization id (D5). `passwordHash` is server-only and is never
273
+ * serialized to a client (stripped from inspect output + URLs).
274
+ */
275
+ export declare interface StorageRef {
276
+ readonly id: string;
277
+ readonly tenantId: string | null;
278
+ /** subject id that created the object (drives the `owner` rule). */
279
+ readonly ownerId: string | null;
280
+ readonly bucket: string;
281
+ readonly key: string;
282
+ readonly contentType: string;
283
+ readonly size: number;
284
+ readonly checksum: string;
285
+ readonly visibility: StorageVisibility;
286
+ readonly accessPolicy: AccessPolicy | null;
287
+ /** `salt:scryptHash` of the file password, or null. Server-only. */
288
+ readonly passwordHash: string | null;
289
+ readonly createdAt: Date;
290
+ /** Pixel width (images). */
291
+ readonly width?: number | null;
292
+ /** Pixel height (images). */
293
+ readonly height?: number | null;
294
+ /** Media duration in seconds (video/audio; populated by an app hook — the
295
+ * core does not bundle a transcoder). */
296
+ readonly duration?: number | null;
297
+ /** A tiny `data:image/webp;base64,…` LQIP placeholder (images). */
298
+ readonly placeholder?: string | null;
299
+ readonly folder?: string | null;
300
+ readonly tags?: ReadonlyArray<string> | null;
301
+ readonly alt?: string | null;
302
+ readonly caption?: string | null;
303
+ /** Parent ref id when this ref is a derivative; null on an original upload. */
304
+ readonly derivedFrom?: string | null;
305
+ /** Derivative label — e.g. `'rendition'` / `'poster'`; null on an original. */
306
+ readonly kind?: string | null;
307
+ }
308
+
309
+ /** An upload was rejected by a constraint (size / content-type / magic-byte
310
+ * mismatch). `reason` is a stable code; `detail` is human-readable. */
311
+ export declare class StorageRejected extends StorageRejected_base {
312
+ }
313
+
314
+ declare const StorageRejected_base: Schema.TaggedErrorClass<StorageRejected, "StorageRejected", {
315
+ readonly _tag: Schema.tag<"StorageRejected">;
316
+ } & {
317
+ /** `'too-large' | 'content-type-not-allowed' | 'content-mismatch'` */
318
+ reason: typeof Schema.String;
319
+ detail: typeof Schema.String;
320
+ }>;
321
+
322
+ /** Scans bytes before they're stored. Return `{ clean:false, threat }` to
323
+ * reject the upload (fails with `StorageScanRejected`, nothing is stored). */
324
+ export declare type StorageScanner = (input: {
325
+ readonly bytes: Uint8Array;
326
+ readonly contentType: string;
327
+ readonly key?: string;
328
+ }) => StorageScanResult | Promise<StorageScanResult> | Effect.Effect<StorageScanResult>;
329
+
330
+ /** An upload was rejected by the virus scanner. */
331
+ export declare class StorageScanRejected extends StorageScanRejected_base {
332
+ }
333
+
334
+ declare const StorageScanRejected_base: Schema.TaggedErrorClass<StorageScanRejected, "StorageScanRejected", {
335
+ readonly _tag: Schema.tag<"StorageScanRejected">;
336
+ } & {
337
+ threat: typeof Schema.String;
338
+ }>;
339
+
340
+ export declare interface StorageScanResult {
341
+ readonly clean: boolean;
342
+ /** Threat name when `clean` is false. */
343
+ readonly threat?: string;
344
+ }
345
+
346
+ export declare class StorageService extends StorageService_base {
347
+ }
348
+
349
+ declare const StorageService_base: Context.TagClass<StorageService, "@voltro/plugin-storage/StorageService", StorageServiceShape>;
350
+
351
+ /** The service handlers consume: `const storage = yield* StorageService`. */
352
+ export declare interface StorageServiceShape {
353
+ readonly put: (input: PutInput) => Effect.Effect<StorageRef, StorageError | StorageRejected | StorageScanRejected>;
354
+ /** Fetch an object's bytes by id. Enforces the tenant guard ONLY — it does
355
+ * NOT run the access policy / grant check. Use this for trusted server-side
356
+ * reads where the caller has already authorized the access; to deliver a
357
+ * PRIVATE object to an end user, go through `mintUrl` (access-checked) and
358
+ * the serve route, never `get` straight off a client-supplied id. */
359
+ readonly get: (id: string, opts?: {
360
+ readonly tenantId?: string | null;
361
+ }) => Effect.Effect<{
362
+ readonly bytes: Uint8Array;
363
+ readonly ref: StorageRef;
364
+ }, StorageError>;
365
+ /** Direct/presigned URL for an object. For PUBLIC objects this is the
366
+ * CDN/bucket URL (no app round-trip).
367
+ *
368
+ * UNCHECKED for PRIVATE objects: it enforces the tenant guard but NOT the
369
+ * access policy / grants, and on a presigning provider (s3/minio) returns a
370
+ * live presigned GET — i.e. a working, ungated download URL. To hand a
371
+ * private object to an end user, use `mintUrl` (it runs `checkAccess`
372
+ * first). Reach for `getUrl` on a private ref only when the caller has
373
+ * already authorized the access itself. */
374
+ readonly getUrl: (id: string, opts?: {
375
+ readonly tenantId?: string | null;
376
+ readonly expiresInSec?: number;
377
+ }) => Effect.Effect<string, StorageError>;
378
+ readonly head: (id: string, opts?: {
379
+ readonly tenantId?: string | null;
380
+ }) => Effect.Effect<StorageRef | null, StorageError>;
381
+ /** Read a byte range `[start, endInclusive]` of an object (HTTP Range / video
382
+ * seeking / memory-bounded delivery). Uses the provider's native `getRange`
383
+ * when present (filesystem / s3 / azure) and otherwise falls back to a
384
+ * buffered `get` + slice (memory / database — small blobs, nothing to seek).
385
+ * Clamps `endInclusive` to the object's last byte. Enforces the tenant guard
386
+ * ONLY, like `get` — the serve route runs the access check before calling it.
387
+ * Fails `StorageError` status 416 for a start past the end of the object. */
388
+ readonly getRange: (id: string, range: {
389
+ readonly start: number;
390
+ readonly endInclusive: number;
391
+ }, opts?: {
392
+ readonly tenantId?: string | null;
393
+ }) => Effect.Effect<{
394
+ readonly bytes: Uint8Array;
395
+ readonly ref: StorageRef;
396
+ readonly totalSize: number;
397
+ }, StorageError>;
398
+ readonly delete: (id: string, opts?: {
399
+ readonly tenantId?: string | null;
400
+ }) => Effect.Effect<void, StorageError>;
401
+ /** Run the access check for `subject`, then return a URL that delivers
402
+ * the bytes: a public/CDN URL, a presigned GET (s3/minio), or an app
403
+ * serve URL carrying a short-lived signed grant token (fs/memory). */
404
+ readonly mintUrl: (id: string, subject: Subject, opts?: {
405
+ readonly password?: string;
406
+ readonly expiresInSec?: number;
407
+ }) => Effect.Effect<string, StorageError | StorageAccessDenied>;
408
+ /** Presigned PUT for direct-to-bucket upload (s3/minio). */
409
+ readonly mintUploadUrl: (input: {
410
+ readonly key: string;
411
+ readonly contentType: string;
412
+ readonly tenantId?: string | null;
413
+ readonly expiresInSec?: number;
414
+ }) => Effect.Effect<string, StorageError>;
415
+ /** Whether `subject` may access `ref` (policy + persisted grants). */
416
+ readonly checkAccess: (ref: StorageRef, subject: Subject, opts?: {
417
+ readonly password?: string;
418
+ readonly permission?: GrantPermission;
419
+ }) => Effect.Effect<boolean, StorageError>;
420
+ /** List / browse / search the stored refs by `folder` prefix, `tags`,
421
+ * `ownerId`, and `tenantId`, newest-first, with offset paging. The
422
+ * first-class media-library / file-manager read — a consumer never has to
423
+ * query `_voltro_storage_refs` by hand. Enforces NO per-ref access policy
424
+ * (it filters the index, it doesn't deliver bytes); scope it to the
425
+ * caller's `tenantId` / `ownerId` for an end-user surface, and deliver any
426
+ * listed private object through `mintUrl`. */
427
+ readonly listRefs: (input?: ListRefsInput) => Effect.Effect<ListRefsResult, StorageError>;
428
+ readonly grant: (input: GrantInput) => Effect.Effect<StorageGrant, StorageError>;
429
+ readonly revoke: (grantId: string) => Effect.Effect<void, StorageError>;
430
+ readonly listGrants: (refId: string) => Effect.Effect<ReadonlyArray<StorageGrant>, StorageError>;
431
+ /** Bytes + object count for a tenant (drives per-tenant quotas + dashboards). */
432
+ readonly usage: (tenantId: string | null) => Effect.Effect<{
433
+ readonly bytes: number;
434
+ readonly count: number;
435
+ }, StorageError>;
436
+ /** Garbage-collect dangling refs whose bytes are gone from the provider
437
+ * (crash between put + insert, or a manual blob delete). Scans up to
438
+ * `limit` recent refs; run repeatedly for a full sweep. `dryRun` reports
439
+ * without deleting. */
440
+ readonly sweepOrphans: (opts?: {
441
+ readonly limit?: number;
442
+ readonly dryRun?: boolean;
443
+ }) => Effect.Effect<{
444
+ readonly scanned: number;
445
+ readonly removed: ReadonlyArray<string>;
446
+ }, StorageError>;
447
+ /** Fetch an external URL SERVER-SIDE and store it as an asset (one-liner CMS
448
+ * migration / "adopt this remote image"). The content-type comes from the
449
+ * response unless overridden. NOTE: this fetches an arbitrary URL from the
450
+ * server — pass only trusted URLs (SSRF); front it with an allow-list if the
451
+ * URL is user-supplied. */
452
+ readonly ingestUrl: (url: string, opts?: {
453
+ readonly contentType?: string;
454
+ readonly tenantId?: string | null;
455
+ readonly ownerId?: string | null;
456
+ readonly visibility?: StorageVisibility;
457
+ readonly key?: string;
458
+ readonly folder?: string;
459
+ readonly tags?: ReadonlyArray<string>;
460
+ readonly alt?: string;
461
+ readonly caption?: string;
462
+ }) => Effect.Effect<StorageRef, StorageError | StorageRejected | StorageScanRejected>;
463
+ /** Mint a presigned direct-to-bucket PUT for a fresh random TEMP key
464
+ * (`_incoming/…`). Returns the URL to PUT raw bytes to + the derived key to
465
+ * hand back to `finalizeUpload`. Only for presigning providers (s3/minio);
466
+ * fails otherwise so the caller can fall back to the through-app route. */
467
+ readonly mintPresignedUpload: (input: {
468
+ readonly contentType: string;
469
+ readonly tenantId?: string | null;
470
+ readonly expiresInSec?: number;
471
+ }) => Effect.Effect<{
472
+ readonly uploadUrl: string;
473
+ readonly key: string;
474
+ readonly expiresInSec: number;
475
+ }, StorageError>;
476
+ /** Promote a blob the client PUT directly to a TEMP `key` into a real ref:
477
+ * fetch it back, run the FULL `put()` pipeline (scan/checksum/derivatives/
478
+ * quota), then delete the temp key. This is where a presigned upload gets
479
+ * scanned + a non-degraded ref — out of band from the direct PUT. On a scan
480
+ * reject the temp bytes are deleted (fail-closed, nothing promoted). */
481
+ readonly finalizeUpload: (input: {
482
+ readonly key: string;
483
+ readonly contentType: string;
484
+ readonly tenantId?: string | null;
485
+ readonly ownerId?: string | null;
486
+ readonly visibility?: StorageVisibility;
487
+ readonly folder?: string;
488
+ readonly tags?: ReadonlyArray<string>;
489
+ readonly alt?: string;
490
+ readonly caption?: string;
491
+ }) => Effect.Effect<StorageRef, StorageError | StorageRejected | StorageScanRejected>;
492
+ /** Store one chunk of a resumable upload as its own durable `_incoming/<id>/
493
+ * <index>` object. Idempotent — re-PUTting the same index overwrites it. */
494
+ readonly putResumableChunk: (input: {
495
+ readonly uploadId: string;
496
+ readonly index: number;
497
+ readonly bytes: Uint8Array;
498
+ readonly tenantId?: string | null;
499
+ }) => Effect.Effect<void, StorageError>;
500
+ /** How far a resumable upload got: probes contiguous chunk objects from index
501
+ * 0 and returns the received byte `offset` + the `nextIndex` to send. Lets a
502
+ * reconnecting client resume instead of restarting. */
503
+ readonly resumableStatus: (input: {
504
+ readonly uploadId: string;
505
+ readonly tenantId?: string | null;
506
+ }) => Effect.Effect<{
507
+ readonly offset: number;
508
+ readonly nextIndex: number;
509
+ }, StorageError>;
510
+ /** Assemble a completed resumable upload: read chunks `0..count-1` in order,
511
+ * concatenate, run the FULL `put()` pipeline, then delete the chunk objects
512
+ * (on success AND on reject — fail-closed). */
513
+ readonly finalizeResumable: (input: {
514
+ readonly uploadId: string;
515
+ readonly count: number;
516
+ readonly contentType: string;
517
+ readonly tenantId?: string | null;
518
+ readonly ownerId?: string | null;
519
+ readonly visibility?: StorageVisibility;
520
+ readonly folder?: string;
521
+ readonly tags?: ReadonlyArray<string>;
522
+ readonly alt?: string;
523
+ readonly caption?: string;
524
+ }) => Effect.Effect<StorageRef, StorageError | StorageRejected | StorageScanRejected>;
525
+ /** Run the configured transcoder over a stored asset and persist each output
526
+ * as a rendition ref (linked via `derivedFrom`). Idempotent-ish: re-running
527
+ * content-addresses identical outputs. No-op (→ `[]`) when no transcoder is
528
+ * configured or the ref is itself a derivative. */
529
+ readonly transcode: (refId: string, opts?: {
530
+ readonly tenantId?: string | null;
531
+ }) => Effect.Effect<ReadonlyArray<StorageRef>, StorageError | StorageRejected | StorageScanRejected>;
532
+ /** List the derivative refs (renditions, posters) produced from `refId`. */
533
+ readonly renditions: (refId: string, opts?: {
534
+ readonly tenantId?: string | null;
535
+ }) => Effect.Effect<ReadonlyArray<StorageRef>, StorageError>;
536
+ /** Begin a multipart session on a presigning provider (s3/minio). Returns the
537
+ * bucket `key` + provider `uploadId` to sign parts against. Fails when the
538
+ * provider can't do multipart (the client falls back to `resumable`). */
539
+ readonly beginMultipartUpload: (input: {
540
+ readonly contentType: string;
541
+ readonly tenantId?: string | null;
542
+ readonly visibility?: StorageVisibility;
543
+ readonly expiresInSec?: number;
544
+ }) => Effect.Effect<{
545
+ readonly uploadId: string;
546
+ readonly key: string;
547
+ }, StorageError>;
548
+ /** Presign a PUT for one part (1-based `partNumber`). */
549
+ readonly signMultipartPart: (input: {
550
+ readonly key: string;
551
+ readonly uploadId: string;
552
+ readonly partNumber: number;
553
+ readonly expiresInSec?: number;
554
+ }) => Effect.Effect<string, StorageError>;
555
+ /** Complete a multipart session → the bucket assembles the object; a ref is
556
+ * registered straight from it (size via HEAD, etag as checksum). No fetch-back,
557
+ * so NO scan/derivatives — scan multi-GB out of band. */
558
+ readonly completeMultipart: (input: {
559
+ readonly key: string;
560
+ readonly uploadId: string;
561
+ readonly parts: ReadonlyArray<{
562
+ readonly partNumber: number;
563
+ readonly etag: string;
564
+ }>;
565
+ readonly contentType: string;
566
+ readonly tenantId?: string | null;
567
+ readonly ownerId?: string | null;
568
+ readonly visibility?: StorageVisibility;
569
+ readonly folder?: string;
570
+ readonly tags?: ReadonlyArray<string>;
571
+ readonly alt?: string;
572
+ readonly caption?: string;
573
+ }) => Effect.Effect<StorageRef, StorageError | StorageRejected>;
574
+ /** Abort a multipart session (client cancelled) — frees the bucket's parts. */
575
+ readonly abortMultipart: (input: {
576
+ readonly key: string;
577
+ readonly uploadId: string;
578
+ }) => Effect.Effect<void, StorageError>;
579
+ }
580
+
581
+ /** `public` = anyone, served direct from bucket/CDN; `private` = gated by
582
+ * the ref's access policy + grants. Default for a new put is `private`. */
583
+ export declare type StorageVisibility = 'public' | 'private';
584
+
585
+ /** Bytes + content type as returned by a provider `get`. */
586
+ export declare interface StoredObject {
587
+ readonly bytes: Uint8Array;
588
+ readonly contentType: string;
589
+ }
590
+
591
+ /** A streamed read from a provider — the object as a {@link ByteStream} plus
592
+ * its content type and, when the backend reports it up front, its size. */
593
+ export declare interface StoredStream {
594
+ readonly stream: ByteStream;
595
+ readonly contentType: string;
596
+ readonly size?: number;
597
+ }
598
+
599
+ /**
600
+ * App-supplied (or first-party `ffmpegTranscoder`) transcode step. Given a
601
+ * source asset's bytes, returns zero or more renditions (720p mp4, webm, HLS,
602
+ * a poster, …). The core bundles no transcoder — wire ffmpeg (or a cloud API).
603
+ * Runs OUT OF BAND (never blocks `put()`); best-effort — a throw/failure/[] just
604
+ * means no renditions.
605
+ */
606
+ export declare type Transcoder = (input: {
607
+ readonly bytes: Uint8Array;
608
+ readonly contentType: string;
609
+ readonly ref: StorageRef;
610
+ }) => ReadonlyArray<Rendition> | Promise<ReadonlyArray<Rendition>> | Effect.Effect<ReadonlyArray<Rendition>>;
611
+
612
+ export declare interface UploadLimits {
613
+ /** Reject uploads larger than this many bytes. */
614
+ readonly maxBytes?: number;
615
+ /** Allowed content types — exact (`application/pdf`) or wildcard
616
+ * (`image/*`). Omit to allow any. */
617
+ readonly allowedContentTypes?: ReadonlyArray<string>;
618
+ /** Verify the bytes' magic number matches the declared `contentType`
619
+ * (blocks e.g. an executable uploaded as `image/png`). Default false. */
620
+ readonly sniff?: boolean;
621
+ }
622
+
623
+ /**
624
+ * App-supplied media probe for video/audio uploads. The core deliberately does
625
+ * NOT bundle ffmpeg/ffprobe (a large binary + licensing) — wire your own
626
+ * (ffprobe, a cloud video API) and the service calls it in `put()` for
627
+ * video/audio content types. Best-effort: a null/throw never blocks the upload.
628
+ */
629
+ export declare type VideoProbe = (input: {
630
+ readonly bytes: Uint8Array;
631
+ readonly contentType: string;
632
+ }) => VideoProbeResult | null | Promise<VideoProbeResult | null> | Effect.Effect<VideoProbeResult | null>;
633
+
634
+ /** What a `VideoProbe` returns for a video/audio upload. All optional — return
635
+ * `null` (or throw) and the upload proceeds with no media metadata. */
636
+ export declare interface VideoProbeResult {
637
+ /** Duration in seconds. */
638
+ readonly duration?: number;
639
+ readonly width?: number;
640
+ readonly height?: number;
641
+ /** A poster frame as a `data:image/…;base64,…` URI — stored as the ref's
642
+ * `placeholder` (so a `<video poster>` / grid preview has an instant image). */
643
+ readonly poster?: string;
644
+ }
645
+
646
+ export { }
package/dist/types.js ADDED
@@ -0,0 +1,16 @@
1
+ import { Context as e, Schema as t } from "effect";
2
+ //#region src/types.ts
3
+ var n = class extends t.TaggedError()("StorageError", {
4
+ provider: t.String,
5
+ message: t.String,
6
+ transient: t.Boolean,
7
+ status: t.optional(t.Number)
8
+ }) {}, r = class extends t.TaggedError()("StorageAccessDenied", {
9
+ refId: t.String,
10
+ reason: t.String
11
+ }) {}, i = class extends t.TaggedError()("StorageRejected", {
12
+ reason: t.String,
13
+ detail: t.String
14
+ }) {}, a = class extends t.TaggedError()("StorageScanRejected", { threat: t.String }) {}, o = class extends e.Tag("@voltro/plugin-storage/StorageService")() {};
15
+ //#endregion
16
+ export { r as StorageAccessDenied, n as StorageError, i as StorageRejected, a as StorageScanRejected, o as StorageService };