@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.
- package/CHANGELOG.md +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +11075 -0
- package/dist/index.d.ts +1255 -0
- package/dist/index.js +2806 -0
- package/dist/rpc.d.ts +272 -0
- package/dist/rpc.js +365 -0
- package/dist/types.d.ts +646 -0
- package/dist/types.js +16 -0
- package/package.json +62 -0
package/dist/index.d.ts
ADDED
|
@@ -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 { }
|