@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/types.d.ts
ADDED
|
@@ -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 };
|