@velajs/storage 0.3.0 → 0.3.2
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 +6 -1
- package/dist/body-BMUcBeQ0.js +93 -0
- package/dist/body-BMUcBeQ0.js.map +1 -0
- package/dist/client/index.d.ts +49 -46
- package/dist/client/index.js +207 -228
- package/dist/client/index.js.map +1 -0
- package/dist/drivers/memory/index.d.ts +17 -13
- package/dist/drivers/memory/index.js +190 -197
- package/dist/drivers/memory/index.js.map +1 -0
- package/dist/drivers/r2/index.d.ts +7 -4
- package/dist/drivers/r2/index.js +162 -160
- package/dist/drivers/r2/index.js.map +1 -0
- package/dist/drivers/r2-http/index.d.ts +23 -19
- package/dist/drivers/r2-http/index.js +52 -45
- package/dist/drivers/r2-http/index.js.map +1 -0
- package/dist/drivers/s3/index.d.ts +79 -5
- package/dist/drivers/s3/index.js +2 -3
- package/dist/index.d.ts +271 -17
- package/dist/index.js +1078 -16
- package/dist/index.js.map +1 -0
- package/dist/middleware/index.d.ts +177 -15
- package/dist/middleware/index.js +553 -8
- package/dist/middleware/index.js.map +1 -0
- package/dist/protocol.types-C5PPBD12.d.ts +80 -0
- package/dist/r2.types-Bw4Ft83t.d.ts +71 -0
- package/dist/retry-DXtO6R9D.js +119 -0
- package/dist/retry-DXtO6R9D.js.map +1 -0
- package/dist/s3-DrE9NrnD.js +557 -0
- package/dist/s3-DrE9NrnD.js.map +1 -0
- package/dist/storage.error-DVCOkJhc.js +49 -0
- package/dist/storage.error-DVCOkJhc.js.map +1 -0
- package/dist/storage.types-BJN7Yp1J.d.ts +288 -0
- package/dist/storagesdk/index.d.ts +73 -69
- package/dist/storagesdk/index.js +117 -130
- package/dist/storagesdk/index.js.map +1 -0
- package/dist/stored-file-2Yl07eRc.js +105 -0
- package/dist/stored-file-2Yl07eRc.js.map +1 -0
- package/dist/testing/index.d.ts +12 -8
- package/dist/testing/index.js +36 -53
- package/dist/testing/index.js.map +1 -0
- package/package.json +62 -48
- package/dist/base64.d.ts +0 -8
- package/dist/base64.js +0 -25
- package/dist/decorators/inject-storage.decorator.d.ts +0 -17
- package/dist/decorators/inject-storage.decorator.js +0 -21
- package/dist/drivers/r2/r2.types.d.ts +0 -67
- package/dist/drivers/r2/r2.types.js +0 -5
- package/dist/drivers/s3/s3-client.d.ts +0 -33
- package/dist/drivers/s3/s3-client.js +0 -154
- package/dist/drivers/s3/s3.driver.d.ts +0 -4
- package/dist/drivers/s3/s3.driver.js +0 -404
- package/dist/drivers/s3/s3.types.d.ts +0 -28
- package/dist/drivers/s3/s3.types.js +0 -1
- package/dist/drivers/s3/xml.d.ts +0 -6
- package/dist/drivers/s3/xml.js +0 -52
- package/dist/http/authorizer.types.d.ts +0 -59
- package/dist/http/authorizer.types.js +0 -1
- package/dist/http/http-helpers.d.ts +0 -22
- package/dist/http/http-helpers.js +0 -80
- package/dist/http/protocol.types.d.ts +0 -76
- package/dist/http/protocol.types.js +0 -1
- package/dist/internal/body.d.ts +0 -18
- package/dist/internal/body.js +0 -90
- package/dist/internal/retry.d.ts +0 -41
- package/dist/internal/retry.js +0 -127
- package/dist/internal/stored-file.d.ts +0 -27
- package/dist/internal/stored-file.js +0 -114
- package/dist/middleware/cache.d.ts +0 -50
- package/dist/middleware/cache.js +0 -101
- package/dist/middleware/compose.d.ts +0 -12
- package/dist/middleware/compose.js +0 -11
- package/dist/middleware/compression.d.ts +0 -16
- package/dist/middleware/compression.js +0 -101
- package/dist/middleware/encryption.d.ts +0 -14
- package/dist/middleware/encryption.js +0 -134
- package/dist/middleware/failover.d.ts +0 -18
- package/dist/middleware/failover.js +0 -52
- package/dist/middleware/retry.d.ts +0 -14
- package/dist/middleware/retry.js +0 -47
- package/dist/middleware/versioning.d.ts +0 -36
- package/dist/middleware/versioning.js +0 -89
- package/dist/middleware/wrap.d.ts +0 -11
- package/dist/middleware/wrap.js +0 -46
- package/dist/object-key.d.ts +0 -17
- package/dist/object-key.js +0 -42
- package/dist/storage.controller.d.ts +0 -21
- package/dist/storage.controller.js +0 -380
- package/dist/storage.error.d.ts +0 -28
- package/dist/storage.error.js +0 -46
- package/dist/storage.facade.d.ts +0 -47
- package/dist/storage.facade.js +0 -443
- package/dist/storage.module.d.ts +0 -46
- package/dist/storage.module.js +0 -113
- package/dist/storage.service.d.ts +0 -42
- package/dist/storage.service.js +0 -86
- package/dist/storage.tokens.d.ts +0 -13
- package/dist/storage.tokens.js +0 -26
- package/dist/storage.types.d.ts +0 -245
- package/dist/storage.types.js +0 -1
|
@@ -1,16 +0,0 @@
|
|
|
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;
|
|
@@ -1,101 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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;
|
|
@@ -1,134 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
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;
|
|
@@ -1,52 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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;
|
package/dist/middleware/retry.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
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;
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { StorageError } from "../storage.error.js";
|
|
2
|
-
import { passthrough } from "./wrap.js";
|
|
3
|
-
export const VERSIONED = Symbol.for('vela.storage.versioned');
|
|
4
|
-
/**
|
|
5
|
-
* Keeps prior versions on overwrite via key-suffixing (no external manifest →
|
|
6
|
-
* no read-modify-write race). Snapshots are cheap when the base driver
|
|
7
|
-
* `supportsServerSideCopy`. Direct presigned uploads bypass snapshotting, so
|
|
8
|
-
* `signedUploadUrl` throws. Reach `listVersions`/`restore`/`deleteVersion` via
|
|
9
|
-
* {@link unwrapVersioning}.
|
|
10
|
-
*/ export function versioning(opts = {}) {
|
|
11
|
-
const prefix = opts.prefix ?? '.vela-versions/';
|
|
12
|
-
let seq = 0;
|
|
13
|
-
const vid = opts.versionId ?? (()=>`${Date.now().toString(36)}-${(seq++).toString(36)}`);
|
|
14
|
-
const vkey = (key, id)=>`${prefix}${key}/${id}`;
|
|
15
|
-
const isVersion = (key)=>key.startsWith(prefix);
|
|
16
|
-
const listVersions = async (inner, key)=>{
|
|
17
|
-
const res = await inner.list({
|
|
18
|
-
prefix: `${prefix}${key}/`
|
|
19
|
-
});
|
|
20
|
-
return res.items.map((item)=>({
|
|
21
|
-
versionId: item.key.slice(item.key.lastIndexOf('/') + 1),
|
|
22
|
-
key: item.key,
|
|
23
|
-
size: item.size,
|
|
24
|
-
lastModified: item.lastModified
|
|
25
|
-
}));
|
|
26
|
-
};
|
|
27
|
-
const snapshot = async (inner, key)=>{
|
|
28
|
-
if (!await inner.exists(key)) return;
|
|
29
|
-
const id = vid();
|
|
30
|
-
if (inner.supportsServerSideCopy) {
|
|
31
|
-
await inner.copy(key, vkey(key, id));
|
|
32
|
-
} else {
|
|
33
|
-
const current = await inner.download(key);
|
|
34
|
-
await inner.upload(vkey(key, id), current.stream(), {
|
|
35
|
-
contentType: current.type
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
if (opts.maxVersions) {
|
|
39
|
-
const versions = (await listVersions(inner, key)).sort((a, b)=>a.versionId < b.versionId ? -1 : 1);
|
|
40
|
-
for (const v of versions.slice(0, Math.max(0, versions.length - opts.maxVersions))){
|
|
41
|
-
await inner.delete(v.key);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
return (inner)=>{
|
|
46
|
-
const driver = passthrough(inner, {
|
|
47
|
-
async upload (k, b, o) {
|
|
48
|
-
await snapshot(inner, k);
|
|
49
|
-
return inner.upload(k, b, o);
|
|
50
|
-
},
|
|
51
|
-
async copy (f, t, o) {
|
|
52
|
-
await snapshot(inner, t);
|
|
53
|
-
return inner.copy(f, t, o);
|
|
54
|
-
},
|
|
55
|
-
async delete (k, o) {
|
|
56
|
-
if (opts.onDelete !== 'purge') await snapshot(inner, k);
|
|
57
|
-
await inner.delete(k, o);
|
|
58
|
-
if (opts.onDelete === 'purge') {
|
|
59
|
-
for (const v of (await listVersions(inner, k)))await inner.delete(v.key);
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
list: opts.hideFromList === false ? undefined : async (o)=>{
|
|
63
|
-
const res = await inner.list(o);
|
|
64
|
-
return {
|
|
65
|
-
items: res.items.filter((i)=>!isVersion(i.key)),
|
|
66
|
-
prefixes: res.prefixes?.filter((p)=>!isVersion(p)),
|
|
67
|
-
cursor: res.cursor
|
|
68
|
-
};
|
|
69
|
-
},
|
|
70
|
-
signedUploadUrl () {
|
|
71
|
-
throw new StorageError('Unsupported', 'versioning: direct uploads bypass snapshots');
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
const versioned = {
|
|
75
|
-
listVersions: (key)=>listVersions(inner, key),
|
|
76
|
-
async restore (key, versionId) {
|
|
77
|
-
await snapshot(inner, key);
|
|
78
|
-
await inner.copy(vkey(key, versionId), key);
|
|
79
|
-
},
|
|
80
|
-
deleteVersion: (key, versionId)=>inner.delete(vkey(key, versionId))
|
|
81
|
-
};
|
|
82
|
-
return Object.assign(driver, {
|
|
83
|
-
[VERSIONED]: versioned
|
|
84
|
-
});
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
/** Retrieve the {@link Versioned} surface from a versioning-wrapped driver (or `undefined`). */ export function unwrapVersioning(driver) {
|
|
88
|
-
return driver[VERSIONED];
|
|
89
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { StorageDriver } from '../storage.types';
|
|
2
|
-
/** A driver middleware: wraps a driver, returns a driver. */
|
|
3
|
-
export type Middleware = (inner: StorageDriver) => StorageDriver;
|
|
4
|
-
/**
|
|
5
|
-
* Build a driver that delegates to `inner`, applying `over` overrides and
|
|
6
|
-
* omitting the methods in `omit`. Explicit delegation (not spread) keeps
|
|
7
|
-
* class-based drivers' `this`. Optional methods are forwarded only when the
|
|
8
|
-
* inner driver has them (so `presence == capability` is preserved), unless
|
|
9
|
-
* omitted (e.g. a body-transforming middleware suppresses multipart/presign).
|
|
10
|
-
*/
|
|
11
|
-
export declare function passthrough(inner: StorageDriver, over?: Partial<StorageDriver>, omit?: ReadonlyArray<keyof StorageDriver>): StorageDriver;
|
package/dist/middleware/wrap.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build a driver that delegates to `inner`, applying `over` overrides and
|
|
3
|
-
* omitting the methods in `omit`. Explicit delegation (not spread) keeps
|
|
4
|
-
* class-based drivers' `this`. Optional methods are forwarded only when the
|
|
5
|
-
* inner driver has them (so `presence == capability` is preserved), unless
|
|
6
|
-
* omitted (e.g. a body-transforming middleware suppresses multipart/presign).
|
|
7
|
-
*/ export function passthrough(inner, over = {}, omit = []) {
|
|
8
|
-
const omitted = new Set(omit);
|
|
9
|
-
const has = (k)=>!omitted.has(k);
|
|
10
|
-
const d = {
|
|
11
|
-
name: over.name ?? inner.name,
|
|
12
|
-
raw: inner.raw,
|
|
13
|
-
reportsUploadProgress: over.reportsUploadProgress ?? inner.reportsUploadProgress,
|
|
14
|
-
supportsRange: over.supportsRange ?? inner.supportsRange,
|
|
15
|
-
supportsDelimiter: over.supportsDelimiter ?? inner.supportsDelimiter,
|
|
16
|
-
supportsMetadata: over.supportsMetadata ?? inner.supportsMetadata,
|
|
17
|
-
supportsCacheControl: over.supportsCacheControl ?? inner.supportsCacheControl,
|
|
18
|
-
supportsServerSideCopy: over.supportsServerSideCopy ?? inner.supportsServerSideCopy,
|
|
19
|
-
signedUrl: over.signedUrl ?? inner.signedUrl,
|
|
20
|
-
upload: over.upload ?? ((k, b, o)=>inner.upload(k, b, o)),
|
|
21
|
-
download: over.download ?? ((k, o)=>inner.download(k, o)),
|
|
22
|
-
head: over.head ?? ((k, o)=>inner.head(k, o)),
|
|
23
|
-
exists: over.exists ?? ((k, o)=>inner.exists(k, o)),
|
|
24
|
-
delete: over.delete ?? ((k, o)=>inner.delete(k, o)),
|
|
25
|
-
copy: over.copy ?? ((f, t, o)=>inner.copy(f, t, o)),
|
|
26
|
-
list: over.list ?? ((o)=>inner.list(o)),
|
|
27
|
-
url: over.url ?? ((k, o)=>inner.url(k, o)),
|
|
28
|
-
signedUploadUrl: over.signedUploadUrl ?? ((k, o)=>inner.signedUploadUrl(k, o))
|
|
29
|
-
};
|
|
30
|
-
if (has('deleteMany') && (over.deleteMany || inner.deleteMany)) {
|
|
31
|
-
d.deleteMany = over.deleteMany ?? ((ks, o)=>inner.deleteMany(ks, o));
|
|
32
|
-
}
|
|
33
|
-
if (has('move') && (over.move || inner.move)) {
|
|
34
|
-
d.move = over.move ?? ((f, t, o)=>inner.move(f, t, o));
|
|
35
|
-
}
|
|
36
|
-
if (has('createMultipartUpload') && (over.createMultipartUpload || inner.createMultipartUpload)) {
|
|
37
|
-
d.createMultipartUpload = over.createMultipartUpload ?? ((k, o)=>inner.createMultipartUpload(k, o));
|
|
38
|
-
}
|
|
39
|
-
if (has('resumeMultipartUpload') && (over.resumeMultipartUpload || inner.resumeMultipartUpload)) {
|
|
40
|
-
d.resumeMultipartUpload = over.resumeMultipartUpload ?? ((k, u)=>inner.resumeMultipartUpload(k, u));
|
|
41
|
-
}
|
|
42
|
-
if (has('signedMultipart') && (over.signedMultipart || inner.signedMultipart)) {
|
|
43
|
-
d.signedMultipart = over.signedMultipart ?? inner.signedMultipart;
|
|
44
|
-
}
|
|
45
|
-
return d;
|
|
46
|
-
}
|
package/dist/object-key.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* True when `key` is a safe object key:
|
|
3
|
-
* - non-empty, not longer than 1024 bytes (S3's limit)
|
|
4
|
-
* - no leading `/`
|
|
5
|
-
* - no `.` / `..` path segments (traversal)
|
|
6
|
-
* - no control characters
|
|
7
|
-
* - no backslashes (Windows-style separators)
|
|
8
|
-
*/
|
|
9
|
-
export declare function isSafeKey(key: string): boolean;
|
|
10
|
-
/** Return `key` if safe, else throw `StorageError('InvalidKey')`. */
|
|
11
|
-
export declare function sanitizeKey(key: string): string;
|
|
12
|
-
/** Normalize a prefix: drop leading/trailing slashes, collapse to '' when empty. */
|
|
13
|
-
export declare function normalizePrefix(prefix: string | undefined): string;
|
|
14
|
-
/** Join a normalized prefix and a key with a single '/'. */
|
|
15
|
-
export declare function joinKey(prefix: string, key: string): string;
|
|
16
|
-
/** Strip a normalized prefix from a stored key (for surfacing to callers). */
|
|
17
|
-
export declare function stripPrefix(prefix: string, key: string): string;
|
package/dist/object-key.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { StorageError } from "./storage.error.js";
|
|
2
|
-
// Object-key hygiene shared by the facade (prefix scoping) and the HTTP
|
|
3
|
-
// controller (post-authorize re-sanitization). The rules block the classic
|
|
4
|
-
// traversal / injection footguns without being so strict that legitimate
|
|
5
|
-
// keys (nested "folders", dots, spaces) are rejected.
|
|
6
|
-
// Control characters: C0 range (0x00-0x1F) plus DEL (0x7F).
|
|
7
|
-
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
8
|
-
/**
|
|
9
|
-
* True when `key` is a safe object key:
|
|
10
|
-
* - non-empty, not longer than 1024 bytes (S3's limit)
|
|
11
|
-
* - no leading `/`
|
|
12
|
-
* - no `.` / `..` path segments (traversal)
|
|
13
|
-
* - no control characters
|
|
14
|
-
* - no backslashes (Windows-style separators)
|
|
15
|
-
*/ export function isSafeKey(key) {
|
|
16
|
-
if (typeof key !== 'string' || key.length === 0 || key.length > 1024) return false;
|
|
17
|
-
if (key.startsWith('/')) return false;
|
|
18
|
-
if (key.includes('\\')) return false;
|
|
19
|
-
if (CONTROL_CHARS.test(key)) return false;
|
|
20
|
-
for (const segment of key.split('/')){
|
|
21
|
-
if (segment === '.' || segment === '..') return false;
|
|
22
|
-
}
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
/** Return `key` if safe, else throw `StorageError('InvalidKey')`. */ export function sanitizeKey(key) {
|
|
26
|
-
if (!isSafeKey(key)) {
|
|
27
|
-
throw new StorageError('InvalidKey', `unsafe object key: ${JSON.stringify(key)}`);
|
|
28
|
-
}
|
|
29
|
-
return key;
|
|
30
|
-
}
|
|
31
|
-
/** Normalize a prefix: drop leading/trailing slashes, collapse to '' when empty. */ export function normalizePrefix(prefix) {
|
|
32
|
-
if (!prefix) return '';
|
|
33
|
-
return prefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
34
|
-
}
|
|
35
|
-
/** Join a normalized prefix and a key with a single '/'. */ export function joinKey(prefix, key) {
|
|
36
|
-
return prefix ? `${prefix}/${key}` : key;
|
|
37
|
-
}
|
|
38
|
-
/** Strip a normalized prefix from a stored key (for surfacing to callers). */ export function stripPrefix(prefix, key) {
|
|
39
|
-
if (!prefix) return key;
|
|
40
|
-
const withSlash = `${prefix}/`;
|
|
41
|
-
return key.startsWith(withSlash) ? key.slice(withSlash.length) : key;
|
|
42
|
-
}
|