@velajs/storage 0.2.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +56 -0
  4. package/dist/base64.d.ts +8 -0
  5. package/dist/base64.js +25 -0
  6. package/dist/client/index.d.ts +50 -0
  7. package/dist/client/index.js +243 -0
  8. package/dist/decorators/inject-storage.decorator.d.ts +17 -0
  9. package/dist/decorators/inject-storage.decorator.js +21 -0
  10. package/dist/drivers/memory/index.d.ts +16 -0
  11. package/dist/drivers/memory/index.js +202 -0
  12. package/dist/drivers/r2/index.d.ts +11 -0
  13. package/dist/drivers/r2/index.js +166 -0
  14. package/dist/drivers/r2/r2.types.d.ts +67 -0
  15. package/dist/drivers/r2/r2.types.js +5 -0
  16. package/dist/drivers/r2-http/index.d.ts +33 -0
  17. package/dist/drivers/r2-http/index.js +52 -0
  18. package/dist/drivers/s3/index.d.ts +5 -0
  19. package/dist/drivers/s3/index.js +3 -0
  20. package/dist/drivers/s3/s3-client.d.ts +33 -0
  21. package/dist/drivers/s3/s3-client.js +154 -0
  22. package/dist/drivers/s3/s3.driver.d.ts +4 -0
  23. package/dist/drivers/s3/s3.driver.js +404 -0
  24. package/dist/drivers/s3/s3.types.d.ts +28 -0
  25. package/dist/drivers/s3/s3.types.js +1 -0
  26. package/dist/drivers/s3/xml.d.ts +6 -0
  27. package/dist/drivers/s3/xml.js +52 -0
  28. package/dist/http/authorizer.types.d.ts +59 -0
  29. package/dist/http/authorizer.types.js +1 -0
  30. package/dist/http/http-helpers.d.ts +22 -0
  31. package/dist/http/http-helpers.js +80 -0
  32. package/dist/http/protocol.types.d.ts +76 -0
  33. package/dist/http/protocol.types.js +1 -0
  34. package/dist/index.d.ts +17 -0
  35. package/dist/index.js +16 -0
  36. package/dist/internal/body.d.ts +18 -0
  37. package/dist/internal/body.js +90 -0
  38. package/dist/internal/retry.d.ts +41 -0
  39. package/dist/internal/retry.js +127 -0
  40. package/dist/internal/stored-file.d.ts +27 -0
  41. package/dist/internal/stored-file.js +114 -0
  42. package/dist/middleware/cache.d.ts +50 -0
  43. package/dist/middleware/cache.js +101 -0
  44. package/dist/middleware/compose.d.ts +12 -0
  45. package/dist/middleware/compose.js +11 -0
  46. package/dist/middleware/compression.d.ts +16 -0
  47. package/dist/middleware/compression.js +101 -0
  48. package/dist/middleware/encryption.d.ts +14 -0
  49. package/dist/middleware/encryption.js +134 -0
  50. package/dist/middleware/failover.d.ts +18 -0
  51. package/dist/middleware/failover.js +52 -0
  52. package/dist/middleware/index.d.ts +15 -0
  53. package/dist/middleware/index.js +8 -0
  54. package/dist/middleware/retry.d.ts +14 -0
  55. package/dist/middleware/retry.js +47 -0
  56. package/dist/middleware/versioning.d.ts +36 -0
  57. package/dist/middleware/versioning.js +89 -0
  58. package/dist/middleware/wrap.d.ts +11 -0
  59. package/dist/middleware/wrap.js +46 -0
  60. package/dist/object-key.d.ts +17 -0
  61. package/dist/object-key.js +42 -0
  62. package/dist/storage.controller.d.ts +21 -0
  63. package/dist/storage.controller.js +380 -0
  64. package/dist/storage.error.d.ts +28 -0
  65. package/dist/storage.error.js +46 -0
  66. package/dist/storage.facade.d.ts +47 -0
  67. package/dist/storage.facade.js +443 -0
  68. package/dist/storage.module.d.ts +46 -0
  69. package/dist/storage.module.js +113 -0
  70. package/dist/storage.service.d.ts +42 -0
  71. package/dist/storage.service.js +86 -0
  72. package/dist/storage.tokens.d.ts +13 -0
  73. package/dist/storage.tokens.js +26 -0
  74. package/dist/storage.types.d.ts +245 -0
  75. package/dist/storage.types.js +1 -0
  76. package/dist/storagesdk/index.d.ts +85 -0
  77. package/dist/storagesdk/index.js +136 -0
  78. package/package.json +116 -0
@@ -0,0 +1,101 @@
1
+ import { createStoredFile } from "../internal/stored-file.js";
2
+ import { passthrough } from "./wrap.js";
3
+ /** In-memory, per-isolate cache with lazy TTL + insertion-order eviction (no timers). */ export class MapCacheStore {
4
+ maxEntries;
5
+ now;
6
+ #map = new Map();
7
+ constructor(maxEntries = 1000, now = Date.now){
8
+ this.maxEntries = maxEntries;
9
+ this.now = now;
10
+ }
11
+ get(key) {
12
+ const e = this.#map.get(key);
13
+ if (!e) return undefined;
14
+ if (e.expiresAt <= this.now()) {
15
+ this.#map.delete(key);
16
+ return undefined;
17
+ }
18
+ return e;
19
+ }
20
+ set(key, value) {
21
+ this.#map.delete(key);
22
+ this.#map.set(key, value);
23
+ if (this.#map.size > this.maxEntries) {
24
+ const oldest = this.#map.keys().next().value;
25
+ if (oldest !== undefined) this.#map.delete(oldest);
26
+ }
27
+ }
28
+ delete(key) {
29
+ this.#map.delete(key);
30
+ }
31
+ }
32
+ function toMeta(f) {
33
+ return {
34
+ key: f.key,
35
+ name: f.name,
36
+ size: f.size,
37
+ type: f.type,
38
+ lastModified: f.lastModified,
39
+ etag: f.etag,
40
+ metadata: f.metadata
41
+ };
42
+ }
43
+ /**
44
+ * Read-through cache for `head` / `exists`. Bodies are never cached (a `head`
45
+ * hit still lazily fetches on body access), and `list` is not cached
46
+ * (cross-prefix invalidation is intractable). Writes invalidate the key.
47
+ */ export function cache(opts = {}) {
48
+ const now = opts.now ?? Date.now;
49
+ const store = opts.store ?? new MapCacheStore(1000, now);
50
+ const ttl = opts.ttlMs ?? 60_000;
51
+ const doHead = opts.cache?.head ?? true;
52
+ const doExists = opts.cache?.exists ?? true;
53
+ const ns = (k)=>`${opts.keyPrefix ?? ''}${k}`;
54
+ return (inner)=>{
55
+ const invalidate = async (...keys)=>{
56
+ await Promise.all(keys.map((k)=>store.delete(ns(k))));
57
+ };
58
+ return passthrough(inner, {
59
+ head: doHead ? async (k, o)=>{
60
+ const hit = await store.get(ns(k));
61
+ if (hit?.kind === 'meta' && hit.meta) {
62
+ return createStoredFile(hit.meta, {
63
+ kind: 'lazy',
64
+ fetch: async ()=>new Response((await inner.download(k)).stream())
65
+ });
66
+ }
67
+ const file = await inner.head(k, o);
68
+ await store.set(ns(k), {
69
+ kind: 'meta',
70
+ meta: toMeta(file),
71
+ expiresAt: now() + ttl
72
+ }, ttl);
73
+ return file;
74
+ } : undefined,
75
+ exists: doExists ? async (k, o)=>{
76
+ const hit = await store.get(ns(k));
77
+ if (hit?.kind === 'exists' && typeof hit.exists === 'boolean') return hit.exists;
78
+ const exists = await inner.exists(k, o);
79
+ await store.set(ns(k), {
80
+ kind: 'exists',
81
+ exists,
82
+ expiresAt: now() + ttl
83
+ }, ttl);
84
+ return exists;
85
+ } : undefined,
86
+ upload: async (k, b, o)=>{
87
+ const r = await inner.upload(k, b, o);
88
+ await invalidate(k);
89
+ return r;
90
+ },
91
+ delete: async (k, o)=>{
92
+ await inner.delete(k, o);
93
+ await invalidate(k);
94
+ },
95
+ copy: async (f, t, o)=>{
96
+ await inner.copy(f, t, o);
97
+ await invalidate(t);
98
+ }
99
+ });
100
+ };
101
+ }
@@ -0,0 +1,12 @@
1
+ import type { StorageDriver } from '../storage.types';
2
+ import type { Middleware } from './wrap';
3
+ /**
4
+ * Compose middlewares over a base driver. Listed innermost → outermost:
5
+ * `compose(base, a, b, c)` yields `c(b(a(base)))`, so on any call `c` runs
6
+ * first (outermost) and `base` last. The same wrapper handles read and write,
7
+ * so the reverse order on download comes for free.
8
+ *
9
+ * There is deliberately NO default preset — each middleware is opt-in, so you
10
+ * never accidentally ship, e.g., compress-then-encrypt (a CRIME/BREACH vector).
11
+ */
12
+ export declare function compose(base: StorageDriver, ...middlewares: Middleware[]): StorageDriver;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Compose middlewares over a base driver. Listed innermost → outermost:
3
+ * `compose(base, a, b, c)` yields `c(b(a(base)))`, so on any call `c` runs
4
+ * first (outermost) and `base` last. The same wrapper handles read and write,
5
+ * so the reverse order on download comes for free.
6
+ *
7
+ * There is deliberately NO default preset — each middleware is opt-in, so you
8
+ * never accidentally ship, e.g., compress-then-encrypt (a CRIME/BREACH vector).
9
+ */ export function compose(base, ...middlewares) {
10
+ return middlewares.reduce((driver, wrap)=>wrap(driver), base);
11
+ }
@@ -0,0 +1,16 @@
1
+ import { type Middleware } from './wrap';
2
+ export type CompressionFormat = 'gzip' | 'deflate' | 'deflate-raw';
3
+ export interface CompressionOptions {
4
+ /** Stream format. Default `gzip`. */
5
+ format?: CompressionFormat;
6
+ /** What to do when `CompressionStream` is unavailable. Default `throw`. */
7
+ onUnavailable?: 'throw' | 'passthrough';
8
+ }
9
+ /**
10
+ * Transparent gzip/deflate via Web Streams `CompressionStream` — fully
11
+ * streaming, opaque mode (v1). The stored object is compressed and tagged
12
+ * `vela-zip`; reads detect the tag and decompress, so pre-existing (untagged)
13
+ * objects pass through unharmed. Range reads / presigned URLs are disabled
14
+ * (the byte stream is not seekable and a direct URL would serve gzip).
15
+ */
16
+ export declare function compression(opts?: CompressionOptions): Middleware;
@@ -0,0 +1,101 @@
1
+ import { byteLengthOf, toStream } from "../internal/body.js";
2
+ import { createStoredFile } from "../internal/stored-file.js";
3
+ import { StorageError } from "../storage.error.js";
4
+ import { passthrough } from "./wrap.js";
5
+ function stripVela(meta) {
6
+ if (!meta) return undefined;
7
+ const out = {};
8
+ for (const [k, v] of Object.entries(meta))if (!k.startsWith('vela-')) out[k] = v;
9
+ return Object.keys(out).length ? out : undefined;
10
+ }
11
+ /**
12
+ * Transparent gzip/deflate via Web Streams `CompressionStream` — fully
13
+ * streaming, opaque mode (v1). The stored object is compressed and tagged
14
+ * `vela-zip`; reads detect the tag and decompress, so pre-existing (untagged)
15
+ * objects pass through unharmed. Range reads / presigned URLs are disabled
16
+ * (the byte stream is not seekable and a direct URL would serve gzip).
17
+ */ export function compression(opts = {}) {
18
+ const format = opts.format ?? 'gzip';
19
+ const available = typeof CompressionStream !== 'undefined' && typeof DecompressionStream !== 'undefined';
20
+ if (!available && (opts.onUnavailable ?? 'throw') === 'throw') {
21
+ throw new StorageError('Unsupported', 'CompressionStream is not available in this runtime');
22
+ }
23
+ return (inner)=>{
24
+ if (!available) return inner; // passthrough mode
25
+ const decompressDownload = async (k, _o)=>{
26
+ const file = await inner.download(k);
27
+ const zip = file.metadata?.['vela-zip'];
28
+ if (!zip) return file; // untagged / legacy object
29
+ const plain = file.stream().pipeThrough(new DecompressionStream(zip));
30
+ const size = file.metadata?.['vela-size'] ? Number(file.metadata['vela-size']) : file.size;
31
+ return createStoredFile({
32
+ key: k,
33
+ name: file.name,
34
+ size,
35
+ type: file.metadata?.['vela-ct'] ?? file.type,
36
+ lastModified: file.lastModified,
37
+ etag: file.etag,
38
+ metadata: stripVela(file.metadata)
39
+ }, {
40
+ kind: 'stream',
41
+ stream: plain
42
+ });
43
+ };
44
+ return passthrough(inner, {
45
+ reportsUploadProgress: false,
46
+ supportsRange: false,
47
+ signedUrl: {
48
+ supported: false,
49
+ upload: false
50
+ },
51
+ async upload (k, body, o) {
52
+ const zipped = toStream(body).pipeThrough(new CompressionStream(format));
53
+ const metadata = {
54
+ ...o?.metadata,
55
+ 'vela-zip': format
56
+ };
57
+ if (o?.contentType) metadata['vela-ct'] = o.contentType;
58
+ const known = byteLengthOf(body);
59
+ if (known != null) metadata['vela-size'] = String(known);
60
+ const r = await inner.upload(k, zipped, {
61
+ ...o,
62
+ metadata: inner.supportsMetadata ? metadata : undefined,
63
+ contentType: 'application/octet-stream'
64
+ });
65
+ return {
66
+ ...r,
67
+ size: known ?? r.size,
68
+ contentType: o?.contentType ?? 'application/octet-stream'
69
+ };
70
+ },
71
+ download: decompressDownload,
72
+ async head (k, o) {
73
+ const file = await inner.head(k, o);
74
+ if (!file.metadata?.['vela-zip']) return file;
75
+ const size = file.metadata['vela-size'] ? Number(file.metadata['vela-size']) : file.size;
76
+ return createStoredFile({
77
+ key: k,
78
+ name: file.name,
79
+ size,
80
+ type: file.metadata['vela-ct'] ?? file.type,
81
+ lastModified: file.lastModified,
82
+ etag: file.etag,
83
+ metadata: stripVela(file.metadata)
84
+ }, {
85
+ kind: 'lazy',
86
+ fetch: async ()=>new Response((await decompressDownload(k)).stream())
87
+ });
88
+ },
89
+ url () {
90
+ throw new StorageError('Unsupported', 'compression: url() would serve compressed bytes');
91
+ },
92
+ signedUploadUrl () {
93
+ throw new StorageError('Unsupported', 'compression: signed uploads would store uncompressed');
94
+ }
95
+ }, [
96
+ 'createMultipartUpload',
97
+ 'resumeMultipartUpload',
98
+ 'signedMultipart'
99
+ ]);
100
+ };
101
+ }
@@ -0,0 +1,14 @@
1
+ import { type Middleware } from './wrap';
2
+ export interface EncryptionOptions {
3
+ /** A 256-bit AES-GCM key: a `CryptoKey` or 32 raw bytes. */
4
+ key: CryptoKey | Uint8Array;
5
+ /** How to treat objects that aren't valid ciphertext (legacy data). Default `throw`. */
6
+ onInvalid?: 'throw' | 'passthrough';
7
+ }
8
+ /**
9
+ * Transparent client-side encryption via Web Crypto AES-GCM. v1 encrypts the
10
+ * WHOLE object (buffered); the stored object is `IV(12) || ciphertext+tag`.
11
+ * Range reads, presigned URLs, and multipart are disabled (a signed URL would
12
+ * hand out ciphertext / accept plaintext, defeating the invariant).
13
+ */
14
+ export declare function encryption(opts: EncryptionOptions): Middleware;
@@ -0,0 +1,134 @@
1
+ import { toBytes } from "../internal/body.js";
2
+ import { createStoredFile } from "../internal/stored-file.js";
3
+ import { StorageError } from "../storage.error.js";
4
+ import { passthrough } from "./wrap.js";
5
+ const IV_BYTES = 12;
6
+ // The Web Crypto typings pin BufferSource to ArrayBuffer-backed views; our byte
7
+ // views are ArrayBufferLike-typed. They are ArrayBuffer-backed at runtime, so a
8
+ // narrowing cast is safe and confined to this helper.
9
+ const asBuf = (u)=>u;
10
+ function stripVela(meta) {
11
+ if (!meta) return undefined;
12
+ const out = {};
13
+ for (const [k, v] of Object.entries(meta))if (!k.startsWith('vela-')) out[k] = v;
14
+ return Object.keys(out).length ? out : undefined;
15
+ }
16
+ async function importKey(key) {
17
+ if (key instanceof Uint8Array) {
18
+ return crypto.subtle.importKey('raw', key, {
19
+ name: 'AES-GCM'
20
+ }, false, [
21
+ 'encrypt',
22
+ 'decrypt'
23
+ ]);
24
+ }
25
+ return key;
26
+ }
27
+ /**
28
+ * Transparent client-side encryption via Web Crypto AES-GCM. v1 encrypts the
29
+ * WHOLE object (buffered); the stored object is `IV(12) || ciphertext+tag`.
30
+ * Range reads, presigned URLs, and multipart are disabled (a signed URL would
31
+ * hand out ciphertext / accept plaintext, defeating the invariant).
32
+ */ export function encryption(opts) {
33
+ const keyPromise = importKey(opts.key);
34
+ return (inner)=>{
35
+ const decryptDownload = async (k, _o)=>{
36
+ const file = await inner.download(k);
37
+ const framed = new Uint8Array(await file.arrayBuffer());
38
+ if (framed.byteLength < IV_BYTES) {
39
+ if (opts.onInvalid === 'passthrough') return file;
40
+ throw new StorageError('Parse', `object too small to be encrypted: ${k}`);
41
+ }
42
+ const iv = framed.subarray(0, IV_BYTES);
43
+ const ciphertext = framed.subarray(IV_BYTES);
44
+ const key = await keyPromise;
45
+ let plain;
46
+ try {
47
+ plain = new Uint8Array(await crypto.subtle.decrypt({
48
+ name: 'AES-GCM',
49
+ iv: asBuf(iv)
50
+ }, key, asBuf(ciphertext)));
51
+ } catch (e) {
52
+ if (opts.onInvalid === 'passthrough') return file;
53
+ throw new StorageError('Provider', `decryption failed: ${k}`, {
54
+ cause: e
55
+ });
56
+ }
57
+ return createStoredFile({
58
+ key: k,
59
+ name: file.name,
60
+ size: plain.byteLength,
61
+ type: file.metadata?.['vela-ct'] ?? file.type,
62
+ lastModified: file.lastModified,
63
+ etag: file.etag,
64
+ metadata: stripVela(file.metadata)
65
+ }, {
66
+ kind: 'bytes',
67
+ bytes: plain
68
+ });
69
+ };
70
+ return passthrough(inner, {
71
+ reportsUploadProgress: false,
72
+ supportsRange: false,
73
+ signedUrl: {
74
+ supported: false,
75
+ upload: false
76
+ },
77
+ async upload (k, body, o) {
78
+ const plaintext = await toBytes(body);
79
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
80
+ const key = await keyPromise;
81
+ const ct = new Uint8Array(await crypto.subtle.encrypt({
82
+ name: 'AES-GCM',
83
+ iv
84
+ }, key, asBuf(plaintext)));
85
+ const framed = new Uint8Array(IV_BYTES + ct.byteLength);
86
+ framed.set(iv, 0);
87
+ framed.set(ct, IV_BYTES);
88
+ const metadata = {
89
+ ...o?.metadata,
90
+ 'vela-enc': 'AES-GCM'
91
+ };
92
+ if (o?.contentType) metadata['vela-ct'] = o.contentType;
93
+ metadata['vela-size'] = String(plaintext.byteLength);
94
+ const r = await inner.upload(k, framed, {
95
+ ...o,
96
+ metadata: inner.supportsMetadata ? metadata : undefined,
97
+ contentType: 'application/octet-stream'
98
+ });
99
+ return {
100
+ ...r,
101
+ size: plaintext.byteLength,
102
+ contentType: o?.contentType ?? 'application/octet-stream'
103
+ };
104
+ },
105
+ download: decryptDownload,
106
+ async head (k, o) {
107
+ const file = await inner.head(k, o);
108
+ const size = file.metadata?.['vela-size'] ? Number(file.metadata['vela-size']) : file.size;
109
+ return createStoredFile({
110
+ key: k,
111
+ name: file.name,
112
+ size,
113
+ type: file.metadata?.['vela-ct'] ?? file.type,
114
+ lastModified: file.lastModified,
115
+ etag: file.etag,
116
+ metadata: stripVela(file.metadata)
117
+ }, {
118
+ kind: 'lazy',
119
+ fetch: async ()=>new Response((await decryptDownload(k)).stream())
120
+ });
121
+ },
122
+ url () {
123
+ throw new StorageError('Unsupported', 'encryption: url() would expose ciphertext');
124
+ },
125
+ signedUploadUrl () {
126
+ throw new StorageError('Unsupported', 'encryption: signed uploads would store plaintext');
127
+ }
128
+ }, [
129
+ 'createMultipartUpload',
130
+ 'resumeMultipartUpload',
131
+ 'signedMultipart'
132
+ ]);
133
+ };
134
+ }
@@ -0,0 +1,18 @@
1
+ import type { StorageDriver } from '../storage.types';
2
+ import { type Middleware } from './wrap';
3
+ export interface FailoverOptions {
4
+ /** Decide whether an error should trigger failover. Default: transient errors. */
5
+ shouldFailover?: (error: unknown) => boolean;
6
+ onFailover?: (info: {
7
+ from: number;
8
+ to: number;
9
+ error: unknown;
10
+ }) => void;
11
+ }
12
+ /**
13
+ * Read-failover across a primary + fallbacks. Reads try each driver in order
14
+ * until one succeeds; writes and presigning go to the primary only (avoids
15
+ * partial/divergent writes). Capabilities are the intersection so the facade
16
+ * never routes a feature to a driver that can't serve it.
17
+ */
18
+ export declare function failover(fallbacks: StorageDriver[], opts?: FailoverOptions): Middleware;
@@ -0,0 +1,52 @@
1
+ import { StorageError } from "../storage.error.js";
2
+ import { passthrough } from "./wrap.js";
3
+ function isTransient(error) {
4
+ return error instanceof StorageError ? error.retryable : true;
5
+ }
6
+ function intersectCapabilities(chain) {
7
+ return {
8
+ supportsRange: chain.every((d)=>!!d.supportsRange),
9
+ supportsDelimiter: chain.every((d)=>!!d.supportsDelimiter),
10
+ supportsMetadata: chain.every((d)=>!!d.supportsMetadata),
11
+ supportsCacheControl: chain.every((d)=>!!d.supportsCacheControl),
12
+ supportsServerSideCopy: chain.every((d)=>!!d.supportsServerSideCopy)
13
+ };
14
+ }
15
+ /**
16
+ * Read-failover across a primary + fallbacks. Reads try each driver in order
17
+ * until one succeeds; writes and presigning go to the primary only (avoids
18
+ * partial/divergent writes). Capabilities are the intersection so the facade
19
+ * never routes a feature to a driver that can't serve it.
20
+ */ export function failover(fallbacks, opts = {}) {
21
+ const should = opts.shouldFailover ?? isTransient;
22
+ return (primary)=>{
23
+ const chain = [
24
+ primary,
25
+ ...fallbacks
26
+ ];
27
+ const read = async (fn)=>{
28
+ let lastError;
29
+ for(let i = 0; i < chain.length; i++){
30
+ try {
31
+ return await fn(chain[i]);
32
+ } catch (e) {
33
+ lastError = e;
34
+ if (!should(e) || i === chain.length - 1) throw e;
35
+ opts.onFailover?.({
36
+ from: i,
37
+ to: i + 1,
38
+ error: e
39
+ });
40
+ }
41
+ }
42
+ throw lastError;
43
+ };
44
+ return passthrough(primary, {
45
+ ...intersectCapabilities(chain),
46
+ download: (k, o)=>read((d)=>d.download(k, o)),
47
+ head: (k, o)=>read((d)=>d.head(k, o)),
48
+ exists: (k, o)=>read((d)=>d.exists(k, o)),
49
+ list: (o)=>read((d)=>d.list(o))
50
+ });
51
+ };
52
+ }
@@ -0,0 +1,15 @@
1
+ export { compose } from './compose';
2
+ export { passthrough } from './wrap';
3
+ export type { Middleware } from './wrap';
4
+ export { retry } from './retry';
5
+ export type { RetryMiddlewareOptions } from './retry';
6
+ export { failover } from './failover';
7
+ export type { FailoverOptions } from './failover';
8
+ export { cache, MapCacheStore } from './cache';
9
+ export type { CacheOptions, CacheStore, CacheEntry, CachedMeta } from './cache';
10
+ export { encryption } from './encryption';
11
+ export type { EncryptionOptions } from './encryption';
12
+ export { compression } from './compression';
13
+ export type { CompressionOptions, CompressionFormat } from './compression';
14
+ export { versioning, unwrapVersioning, VERSIONED } from './versioning';
15
+ export type { VersioningOptions, Versioned, VersionInfo } from './versioning';
@@ -0,0 +1,8 @@
1
+ export { compose } from "./compose.js";
2
+ export { passthrough } from "./wrap.js";
3
+ export { retry } from "./retry.js";
4
+ export { failover } from "./failover.js";
5
+ export { cache, MapCacheStore } from "./cache.js";
6
+ export { encryption } from "./encryption.js";
7
+ export { compression } from "./compression.js";
8
+ export { versioning, unwrapVersioning, VERSIONED } from "./versioning.js";
@@ -0,0 +1,14 @@
1
+ import type { RetryOptions } from '../storage.types';
2
+ import { type Middleware } from './wrap';
3
+ export interface RetryMiddlewareOptions {
4
+ /** Default retry policy for driver operations. Per-call `opts.retries` wins. */
5
+ retries?: RetryOptions;
6
+ }
7
+ /**
8
+ * Retry transient failures at the driver level. Reads and idempotent ops
9
+ * always retry; an upload retries only when its body is replayable (not a
10
+ * one-shot stream). Note: the `Storage` facade also retries per
11
+ * `OperationOptions.retries` — use this middleware when driving a driver
12
+ * directly, or set the facade's retries to 0 to avoid compounding.
13
+ */
14
+ export declare function retry(opts?: RetryMiddlewareOptions): Middleware;
@@ -0,0 +1,47 @@
1
+ import { isStream } from "../internal/body.js";
2
+ import { runWithRetry } from "../internal/retry.js";
3
+ import { passthrough } from "./wrap.js";
4
+ /**
5
+ * Retry transient failures at the driver level. Reads and idempotent ops
6
+ * always retry; an upload retries only when its body is replayable (not a
7
+ * one-shot stream). Note: the `Storage` facade also retries per
8
+ * `OperationOptions.retries` — use this middleware when driving a driver
9
+ * directly, or set the facade's retries to 0 to avoid compounding.
10
+ */ export function retry(opts = {}) {
11
+ const base = opts.retries ?? 3;
12
+ const run = (o, fn)=>runWithRetry(fn, {
13
+ retries: o?.retries ?? base,
14
+ timeout: o?.timeout,
15
+ signal: o?.signal
16
+ });
17
+ return (inner)=>passthrough(inner, {
18
+ download: (k, o)=>run(o, (s)=>inner.download(k, {
19
+ ...o,
20
+ signal: s
21
+ })),
22
+ head: (k, o)=>run(o, (s)=>inner.head(k, {
23
+ ...o,
24
+ signal: s
25
+ })),
26
+ exists: (k, o)=>run(o, (s)=>inner.exists(k, {
27
+ ...o,
28
+ signal: s
29
+ })),
30
+ list: (o)=>run(o, (s)=>inner.list({
31
+ ...o,
32
+ signal: s
33
+ })),
34
+ delete: (k, o)=>run(o, (s)=>inner.delete(k, {
35
+ ...o,
36
+ signal: s
37
+ })),
38
+ copy: (f, t, o)=>run(o, (s)=>inner.copy(f, t, {
39
+ ...o,
40
+ signal: s
41
+ })),
42
+ upload: (k, b, o)=>isStream(b) ? inner.upload(k, b, o) : run(o, (s)=>inner.upload(k, b, {
43
+ ...o,
44
+ signal: s
45
+ }))
46
+ });
47
+ }
@@ -0,0 +1,36 @@
1
+ import type { StorageDriver } from '../storage.types';
2
+ import { type Middleware } from './wrap';
3
+ export interface VersionInfo {
4
+ versionId: string;
5
+ key: string;
6
+ size: number;
7
+ lastModified?: number;
8
+ }
9
+ /** Extra surface a versioning-wrapped driver exposes (reached via {@link unwrapVersioning}). */
10
+ export interface Versioned {
11
+ listVersions(key: string): Promise<VersionInfo[]>;
12
+ restore(key: string, versionId: string): Promise<void>;
13
+ deleteVersion(key: string, versionId: string): Promise<void>;
14
+ }
15
+ export declare const VERSIONED: unique symbol;
16
+ export interface VersioningOptions {
17
+ /** Prefix under which prior versions are stored. Default `.vela-versions/`. */
18
+ prefix?: string;
19
+ versionId?: () => string;
20
+ /** Keep at most this many versions per key (prunes oldest). */
21
+ maxVersions?: number;
22
+ /** Hide the version prefix from `list()`. Default true. */
23
+ hideFromList?: boolean;
24
+ /** On delete: snapshot first (default) or purge all versions. */
25
+ onDelete?: 'snapshot' | 'purge';
26
+ }
27
+ /**
28
+ * Keeps prior versions on overwrite via key-suffixing (no external manifest →
29
+ * no read-modify-write race). Snapshots are cheap when the base driver
30
+ * `supportsServerSideCopy`. Direct presigned uploads bypass snapshotting, so
31
+ * `signedUploadUrl` throws. Reach `listVersions`/`restore`/`deleteVersion` via
32
+ * {@link unwrapVersioning}.
33
+ */
34
+ export declare function versioning(opts?: VersioningOptions): Middleware;
35
+ /** Retrieve the {@link Versioned} surface from a versioning-wrapped driver (or `undefined`). */
36
+ export declare function unwrapVersioning(driver: StorageDriver): Versioned | undefined;